diff --git a/src/front-end.jsx b/src/front-end.jsx index 9c55358..44d0cf2 100644 --- a/src/front-end.jsx +++ b/src/front-end.jsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import Wordpress from 'wordpress'; import { Route } from 'routing'; import 'style.scss'; @@ -10,154 +10,120 @@ import SideNav from 'widgets/side-nav'; import TopNav from 'widgets/top-nav'; import ErrorBoundary from 'widgets/error-boundary'; -class FrontEnd extends PureComponent { - static displayName = 'FrontEnd'; +function FrontEnd(props) { + const { routeManager, dataSource, ssr } = props; + const [ routeChange, setRouteChange ] = useState(); + const [ wpChange, setWPChange ] = useState(); + const route = useMemo(() => { + return new Route(routeManager, dataSource); + }, [ routeManager, dataSource, routeChange ]); + const wp = useMemo(() => { + return new Wordpress(dataSource, ssr); + }, [ dataSource, ssr, wpChange ]); + const [ sideNavCollapsed, collapseSideNav ] = useState(true); + const [ topNavCollapsed, collapseTopNav ] = useState(false); - constructor(props) { - super(props); - let { routeManager, dataSource } = this.props; - this.state = { - route: new Route(routeManager, dataSource), - wp: new Wordpress(dataSource, props.ssr), - sideNavCollapsed: true, - topNavCollapsed: false, + useEffect(() => { + routeManager.addEventListener('change', setRouteChange); + dataSource.addEventListener('change', setWPChange); + + return () => { + routeManager.addEventListener('change', setRouteChange); + dataSource.addEventListener('change', setWPChange); }; - } - - /** - * Render the application - * - * @return {VNode} - */ - render() { - let { route, wp } = this.state; - let { topNavCollapsed } = this.state; - let { sideNavCollapsed } = this.state; - let PageComponent = route.params.module.default; - let classNames = []; - if (topNavCollapsed) { - classNames.push('top-collapsed'); - } - if (sideNavCollapsed) { - classNames.push('side-collapsed'); - } - let key = route.url; - return ( -
- - - -
- -
-
-
-
- ); - } - - /** - * Added change handlers when component mounts - */ - componentDidMount() { - let { routeManager, dataSource } = this.props; - routeManager.addEventListener('change', this.handleRouteChange); - dataSource.addEventListener('change', this.handleDataSourceChange); - document.addEventListener('scroll', this.handleScroll); - - if (typeof(window) === 'object') { - let Hammer = require('hammerjs'); - let hammer = new Hammer(document.body, { cssProps: { userSelect: 'auto' } }); - hammer.on('swipeleft', this.handleSwipeLeft); - hammer.on('swiperight', this.handleSwipeRight); - } - } - - componentDidUpdate(prevProps, prevState) { - let { dataSource, ssr } = this.props; - let { route } = this.state; - if (prevProps.ssr !== ssr) { - this.setState({ wp: new Wordpress(dataSource, ssr) }); - } - if (prevState.route !== route) { - if (!(prevState.route.history.length < route.history.length)) { - // not going backward - if (document.body.parentElement.scrollTop > 0) { - document.body.parentElement.scrollTop = 0; - } else if (document.body.scrollTop > 0) { - document.body.scrollTop = 0; - } - } - } - } - - /** - * Called when the data source changes - * - * @param {RelaksWordpressDataSourceEvent} evt - */ - handleDataSourceChange = (evt) => { - this.setState({ wp: new Wordpress(evt.target) }); - } - - /** - * Called when the route changes - * - * @param {RelaksRouteManagerEvent} evt - */ - handleRouteChange = (evt) => { - let { dataSource } = this.props; - this.setState({ route: new Route(evt.target, dataSource) }); - } - - /** - * Called when the user scrolls the page contents - * - * @param {Event} evt - */ - handleScroll = (evt) => { - let { topNavCollapsed } = this.state; - let container = document.body; - let previousPos = this.previousScrollPosition || 0; - let currentPos = container.scrollTop; - if (currentPos === 0 && container.parentNode.scrollTop > 0) { - currentPos = container.parentNode.scrollTop; - } - let delta = currentPos - previousPos; - if (delta > 0) { - if (!topNavCollapsed) { - // check to see if we have scroll down efficiently, so that - // hidden the top nav won't reveal white space - let pageContainer = document.getElementsByClassName('page-container')[0]; - let page = (pageContainer) ? pageContainer.firstChild : null; - if (page) { - let pageRect = page.getBoundingClientRect(); - if (pageRect.top <= 40) { - this.setState({ topNavCollapsed: true }); + }); + useEffect(() => { + let previousPos = getScrollPos(); + const handleScroll = (evt) => { + const currentPos = getScrollPos(); + const delta = currentPos - previousPos; + if (delta > 0) { + if (!topNavCollapsed) { + // check to see if we have scroll down efficiently, so that + // hidden the top nav won't reveal white space + const pageContainer = document.getElementsByClassName('page-container')[0]; + const page = (pageContainer) ? pageContainer.firstChild : null; + if (page) { + const pageRect = page.getBoundingClientRect(); + if (pageRect.top <= 40) { + collapseTopNav(true); + } + } else { + collapseTopNav(true); } - } else { - this.setState({ topNavCollapsed: true }); + } + } else if (delta < -10) { + if (topNavCollapsed) { + collapseTopNav(false); } } - } else { - if (topNavCollapsed) { - this.setState({ topNavCollapsed: false }); - } + previousPos = currentPos; + }; + document.addEventListener('scroll', handleScroll); + + return () => { + document.removeEventListener('scroll', handleScroll); + }; + }); + useEffect(() => { + if (typeof(window) === 'object') { + const handleSwipeLeft = (evt) => { + if (!sideNavCollapsed) { + collapseSideNav(true); + } + }; + const handleSwipeRight = (evt) => { + if (sideNavCollapsed) { + collapseSideNav(false); + } + }; + + const Hammer = require('hammerjs'); + const hammer = new Hammer(document.body, { cssProps: { userSelect: 'auto' } }); + hammer.on('swipeleft', handleSwipeLeft); + hammer.on('swiperight', handleSwipeRight); + + return () => { + + }; + } + }); + + const PageComponent = route.params.module.default; + const classNames = []; + if (topNavCollapsed) { + classNames.push('top-collapsed'); + } + if (sideNavCollapsed) { + classNames.push('side-collapsed'); + } + const key = route.url; + return ( +
+ + + +
+ +
+
+
+
+ ); + + function getScrollPos() { + let pos = document.body.scrollTop; + if (pos === 0 && document.body.parentNode.scrollTop > 0) { + pos = document.body.parentNode.scrollTop; } - this.previousScrollPosition = currentPos; + return pos; } - handleSwipeLeft = (evt) => { - let { sideNavCollapsed } = this.state; - if (!sideNavCollapsed) { - this.setState({ sideNavCollapsed: true }); - } - } - - handleSwipeRight = (evt) => { - let { sideNavCollapsed } = this.state; - if (sideNavCollapsed) { - this.setState({ sideNavCollapsed: false }); + function resetScrollPos() { + if (document.body.parentElement.scrollTop > 0) { + document.body.parentElement.scrollTop = 0; + } else if (document.body.scrollTop > 0) { + document.body.scrollTop = 0; } } } @@ -166,3 +132,7 @@ export { FrontEnd as default, FrontEnd }; + +if (process.env.NODE_ENV !== 'production') { + require('./props'); +} diff --git a/src/main.js b/src/main.js index 5385d8f..e878b8a 100644 --- a/src/main.js +++ b/src/main.js @@ -6,50 +6,50 @@ import { Route, routes } from 'routing'; import WordpressDataSource from 'relaks-wordpress-data-source'; import RouteManager from 'relaks-route-manager'; import { harvest } from 'relaks-harvest'; -import Relaks, { plant } from 'relaks'; +import { plant } from 'relaks/hooks'; if (process.env.TARGET === 'browser') { async function initialize(evt) { // create data source - let host = process.env.DATA_HOST || `${location.protocol}//${location.host}`; - let basePath = process.env.BASE_PATH; - let dataSource = new WordpressDataSource({ + const host = process.env.DATA_HOST || `${location.protocol}//${location.host}`; + const basePath = process.env.BASE_PATH; + const dataSource = new WordpressDataSource({ baseURL: host + basePath + 'json', }); dataSource.activate(); // create route manager - let routeManager = new RouteManager({ + const routeManager = new RouteManager({ routes, basePath, useHashFallback: (location.protocol !== 'http:' && location.protocol !== 'https:'), }); routeManager.addEventListener('beforechange', (evt) => { - let route = new Route(routeManager, dataSource); + const route = new Route(routeManager, dataSource); evt.postponeDefault(route.setParameters(evt, true)); }); routeManager.activate(); await routeManager.start(); - let container = document.getElementById('react-container'); + const container = document.getElementById('react-container'); if (!process.env.DATA_HOST) { // there is SSR support when we're fetching data from the same host // as the HTML page - let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' }); - let seeds = await harvest(ssrElement, { seeds: true }); + const ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' }); + const seeds = await harvest(ssrElement, { seeds: true }); plant(seeds); hydrate(ssrElement, container); } - let csrElement = createElement(FrontEnd, { dataSource, routeManager }); + const csrElement = createElement(FrontEnd, { dataSource, routeManager }); render(csrElement, container); // check for changes periodically - let mtimeURL = host + basePath + '.mtime'; + const mtimeURL = host + basePath + '.mtime'; let mtimeLast; for (;;) { try { - let res = await fetch(mtimeURL); - let mtime = await res.text(); + const res = await fetch(mtimeURL); + const mtime = await res.text(); if (mtime !== mtimeLast) { if (mtimeLast) { dataSource.invalidate(); @@ -65,28 +65,28 @@ if (process.env.TARGET === 'browser') { window.addEventListener('load', initialize); } else if (process.env.TARGET === 'node') { async function serverSideRender(options) { - let basePath = process.env.BASE_PATH; - let dataSource = new WordpressDataSource({ + const basePath = process.env.BASE_PATH; + const dataSource = new WordpressDataSource({ baseURL: options.host + basePath + 'json', fetchFunc: options.fetch, }); dataSource.activate(); - let routeManager = new RouteManager({ + const routeManager = new RouteManager({ routes, basePath, }); routeManager.addEventListener('beforechange', (evt) => { - let route = new Route(routeManager, dataSource); + const route = new Route(routeManager, dataSource); evt.postponeDefault(route.setParameters(evt, false)); }); routeManager.activate(); await routeManager.start(options.path); - let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: options.target }); + const ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: options.target }); return harvest(ssrElement); } exports.render = serverSideRender; exports.basePath = process.env.BASE_PATH; -} +} \ No newline at end of file diff --git a/src/pages/archive-page.jsx b/src/pages/archive-page.jsx index 422e7e9..4458240 100644 --- a/src/pages/archive-page.jsx +++ b/src/pages/archive-page.jsx @@ -1,35 +1,24 @@ import Moment from 'moment'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import Breadcrumb from 'widgets/breadcrumb'; import PostList from 'widgets/post-list'; -class ArchivePage extends AsyncComponent { - static displayName = 'ArchivePage'; +async function ArchivePage(props) { + const { wp, route } = props; + const { date } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { date } = route.params; - let props = { route }; - meanwhile.show(); - props.posts = await wp.fetchPostsInMonth(date); - return ; - } -} + render(); + const posts = await wp.fetchPostsInMonth(date); + render(); -class ArchivePageSync extends PureComponent { - static displayName = 'ArchivePageSync'; - - render() { - let { route, posts } = this.props; - let { date } = route.params; - let month = Moment(new Date(date.year, date.month - 1, 1)); - let monthLabel = month.format('MMMM YYYY'); - let trail = [ { label: 'Archives' }, { label: monthLabel } ]; - return ( + function render() { + const month = Moment(new Date(date.year, date.month - 1, 1)); + const monthLabel = month.format('MMMM YYYY'); + const trail = [ { label: 'Archives' }, { label: monthLabel } ]; + show(
@@ -38,22 +27,9 @@ class ArchivePageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - ArchivePage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - ArchivePageSync.propTypes = { - posts: PropTypes.arrayOf(PropTypes.object), - month: PropTypes.instanceOf(Moment), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(ArchivePage); export { - ArchivePage as default, - ArchivePage, - ArchivePageSync, + component as default, + component as ArchivePage, }; diff --git a/src/pages/category-page.jsx b/src/pages/category-page.jsx index 141c52b..5b79edb 100644 --- a/src/pages/category-page.jsx +++ b/src/pages/category-page.jsx @@ -1,44 +1,34 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import Breadcrumb from 'widgets/breadcrumb'; import PostList from 'widgets/post-list'; -class CategoryPage extends AsyncComponent { - static displayName = 'CategoryPage'; +async function CategoryPage(props) { + const { wp, route } = props; + const { categorySlug } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { categorySlug } = route.params; - let props = { route }; - meanwhile.show(); - props.category = await wp.fetchCategory(categorySlug); - props.parentCategories = await wp.fetchParentCategories(props.category); - meanwhile.show(); - props.posts = await wp.fetchPostsInCategory(props.category); - return ; - } -} + render(); + const category = await wp.fetchCategory(categorySlug); + const parentCategories = await wp.fetchParentCategories(category); + render(); + const posts = await wp.fetchPostsInCategory(category); + render(); -class CategoryPageSync extends PureComponent { - static displayName = 'CategoryPageSync'; - - render() { - let { route, posts, category, parentCategories } = this.props; - let trail = [ { label: 'Categories' } ]; - let categoryLabel = _.get(category, 'name', ''); + function render() { + const trail = [ { label: 'Categories' } ]; + const categoryLabel = _.get(category, 'name', ''); if (parentCategories) { for (let parentCategory of parentCategories) { - let label = _.get(parentCategory, 'name', ''); - let url = route.prefetchObjectURL(parentCategory); + const label = _.get(parentCategory, 'name', ''); + const url = route.prefetchObjectURL(parentCategory); trail.push({ label, url }); } trail.push({ label: categoryLabel }); } - return ( + show(
@@ -47,23 +37,9 @@ class CategoryPageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - CategoryPage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - CategoryPageSync.propTypes = { - category: PropTypes.object, - parentCategories: PropTypes.arrayOf(PropTypes.object), - posts: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(CategoryPage); export { - CategoryPage as default, - CategoryPage, - CategoryPageSync, + component as default, + component as CategoryPage, }; diff --git a/src/pages/page-page.jsx b/src/pages/page-page.jsx index 854cb1c..b9d5a7e 100644 --- a/src/pages/page-page.jsx +++ b/src/pages/page-page.jsx @@ -1,44 +1,34 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import HTML from 'widgets/html'; import Breadcrumb from 'widgets/breadcrumb'; import PageView from 'widgets/page-view'; import PageList from 'widgets/page-list'; -class PagePage extends AsyncComponent { - static displayName = 'PagePage'; +async function PagePage(props) { + const { wp, route } = props; + const { pageSlug } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { pageSlug } = route.params; - let props = { route }; - meanwhile.show(); - props.page = await wp.fetchPage(pageSlug); - props.parentPages = await wp.fetchParentPages(props.page); - meanwhile.show(); - props.childPages = await wp.fetchChildPages(props.page); - return ; - } -} + render(); + const page = await wp.fetchPage(pageSlug); + const parentPages = await wp.fetchParentPages(page); + render(); + const childPages = await wp.fetchChildPages(page); + render(); -class PagePageSync extends PureComponent { - static displayName = 'PagePageSync'; - - render() { - let { route, page, parentPages, childPages } = this.props; - let trail = []; + function render() { + const trail = []; if (parentPages) { for (let parentPage of parentPages) { - let title = _.get(parentPage, 'title.rendered', ''); - let url = route.prefetchObjectURL(parentPage); + const title = _.get(parentPage, 'title.rendered', ''); + const url = route.prefetchObjectURL(parentPage); trail.push({ label: , url }) } } - return ( + show(
@@ -48,23 +38,9 @@ class PagePageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PagePage.propTypes = { - wp: PropTypes.instanceOf(WordPress).isRequired, - route: PropTypes.instanceOf(Route).isRequired, - }; - PagePageSync.propTypes = { - page: PropTypes.object, - parentPages: PropTypes.arrayOf(PropTypes.object), - childPages: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route).isRequired, - }; -} +const component = Relaks(PagePage); export { - PagePage as default, - PagePage, - PagePageSync, + component as default, + component as PagePage, }; diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx index 15dbf7e..639f2f9 100644 --- a/src/pages/post-page.jsx +++ b/src/pages/post-page.jsx @@ -1,46 +1,59 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import Breadcrumb from 'widgets/breadcrumb'; import PostView from 'widgets/post-view'; import TagList from 'widgets/tag-list'; import CommentSection from 'widgets/comment-section'; -class PostPage extends AsyncComponent { - static displayName = 'PostPage'; +async function PostPage(props) { + const { wp, route } = props; + const { postSlug } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { postSlug } = route.params; - let props = { route }; - meanwhile.show(); - props.post = await wp.fetchPost(postSlug); - meanwhile.show(); - props.categories = await this.findCategoryChain(props.post); - meanwhile.show(); - props.author = await wp.fetchAuthor(props.post); - meanwhile.show(); - props.tags = await wp.fetchTagsOfPost(props.post); - if (!wp.ssr) { - meanwhile.show(); - props.comments = await wp.fetchComments(props.post); - } - return ; + render(); + const post = await wp.fetchPost(postSlug); + render(); + const categories = await findCategoryChain(post); + render(); + const author = await wp.fetchAuthor(post); + render(); + const tags = await wp.fetchTagsOfPost(post); + render() + let comments; + if (!wp.ssr) { + comments = await wp.fetchComments(post); + render(); } - async findCategoryChain(post) { + function render() { + const trail = [ { label: 'Categories' } ]; + if (categories) { + for (let category of categories) { + const label = _.get(category, 'name', ''); + const url = route.prefetchObjectURL(category); + trail.push({ label, url }); + } + } + show( +
+ + + + +
+ ); + } + + async function findCategoryChain(post) { if (!post) return []; - let ids = post.categories; - let { wp, route } = this.props; - let allCategories = await wp.fetchCategories(); + const allCategories = await wp.fetchCategories(); // add categories, including their parents as well - let applicable = []; - let include = (id) => { + const applicable = []; + const include = (id) => { let category = _.find(allCategories, { id }) if (category) { if (!_.includes(applicable, category)) { @@ -50,20 +63,20 @@ class PostPage extends AsyncComponent { include(category.parent); } }; - for (let id of ids) { + for (let id of post.categories) { include(id); } // see how recently a category was visited - let historyIndex = (category) => { - let predicate = { params: { categorySlug: category.slug }}; + const historyIndex = (category) => { + const predicate = { params: { categorySlug: category.slug }}; return _.findLastIndex(route.history, predicate); }; // see how deep a category is - let depth = (category) => { + const depth = (category) => { if (category.parent) { - let predicate = { id: category.parent }; - let parent = _.find(allCategories, predicate); + const predicate = { id: category.parent }; + const parent = _.find(allCategories, predicate); if (parent) { return depth(parent) + 1; } @@ -74,10 +87,10 @@ class PostPage extends AsyncComponent { // order applicable categories based on how recently it was visited, // how deep it is, and alphabetically; the first criteria makes our // breadcrumb works more sensibly - applicable = _.orderBy(applicable, [ historyIndex, depth, 'name' ], [ 'desc', 'desc', 'asc' ]); - let anchorCategory = _.first(applicable); + const ordered = _.orderBy(applicable, [ historyIndex, depth, 'name' ], [ 'desc', 'desc', 'asc' ]); + const anchorCategory = _.first(ordered); - let trail = []; + const trail = []; if (anchorCategory) { // add category and its ancestors for (let c = anchorCategory; c; c = _.find(applicable, { id: c.parent })) { @@ -94,47 +107,9 @@ class PostPage extends AsyncComponent { } } -class PostPageSync extends PureComponent { - static displayName = 'PostPageSync'; - - render() { - let { route, categories, post, author, tags, comments } = this.props; - let trail = [ { label: 'Categories' } ]; - for (let category of categories) { - let label = _.get(category, 'name', ''); - let url = route.prefetchObjectURL(category); - trail.push({ label, url }); - } - return ( -
- - - - -
- ); - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PostPage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - PostPageSync.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), - tags: PropTypes.arrayOf(PropTypes.object), - post: PropTypes.object, - author: PropTypes.object, - comments: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(PostPage); export { - PostPage as default, - PostPage, - PostPageSync, + component as default, + component as PostPage, }; diff --git a/src/pages/search-page.jsx b/src/pages/search-page.jsx index ca253cd..76e7b84 100644 --- a/src/pages/search-page.jsx +++ b/src/pages/search-page.jsx @@ -1,42 +1,31 @@ -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import Breadcrumb from 'widgets/breadcrumb'; import PostList from 'widgets/post-list'; -class SearchPage extends AsyncComponent { - static displayName = 'SearchPage'; +async function SearchPage(props) { + const { wp, route } = props; + const { search } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { search } = route.params; - let props = { route }; - meanwhile.show(); - props.posts = await wp.fetchMatchingPosts(search); - return ; - } -} + render(); + const posts = await wp.fetchMatchingPosts(search); + render(); -class SearchPageSync extends PureComponent { - static displayName = 'SearchPageSync'; - - render() { - let { route, posts } = this.props; - let { search } = route.params; - let trail = [ { label: 'Search' } ]; + function render() { + const trail = [ { label: 'Search' } ]; if (posts) { - let count = posts.total; + const count = posts.total; if (typeof(count) === 'number') { - let s = (count === 1) ? '' : 's'; - let msg = `${count} matching article${s}`; + const s = (count === 1) ? '' : 's'; + const msg = `${count} matching article${s}`; trail.push({ label: msg }); } } else { trail.push({ label: '...' }); } - return ( + show(
@@ -45,22 +34,9 @@ class SearchPageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - SearchPage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - SearchPageSync.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), - posts: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(SearchPage); export { - SearchPage as default, - SearchPage, - SearchPageSync, + component as default, + component as SearchPage, }; diff --git a/src/pages/tag-page.jsx b/src/pages/tag-page.jsx index 3eaf120..69028bd 100644 --- a/src/pages/tag-page.jsx +++ b/src/pages/tag-page.jsx @@ -1,35 +1,25 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import Breadcrumb from 'widgets/breadcrumb'; import PostList from 'widgets/post-list'; -class TagPage extends AsyncComponent { - static displayName = 'TagPage'; +async function TagPage(props) { + const { wp, route } = props; + const { tagSlug } = route.params; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { tagSlug } = route.params; - let props = { route }; - meanwhile.show(); - props.tag = await wp.fetchTag(tagSlug); - meanwhile.show(); - props.posts = await wp.fetchPostsWithTag(props.tag); - return ; - } -} + render(); + const tag = await wp.fetchTag(tagSlug); + render(); + const posts = await wp.fetchPostsWithTag(tag); + render(); -class TagPageSync extends PureComponent { - static displayName = 'TagPageSync'; - - render() { - let { route, posts, tag } = this.props; - let tagLabel = _.get(tag, 'name', ''); - let trail = [ { label: 'Tags' }, { label: tagLabel } ]; - return ( + function render() { + const tagLabel = _.get(tag, 'name', ''); + const trail = [ { label: 'Tags' }, { label: tagLabel } ]; + show(
@@ -38,22 +28,9 @@ class TagPageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - TagPage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - TagPageSync.propTypes = { - tag: PropTypes.object, - posts: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(TagPage); export { - TagPage as default, - TagPage, - TagPageSync, + component as default, + component as TagPage, }; diff --git a/src/pages/welcome-page.jsx b/src/pages/welcome-page.jsx index 15ac795..9ebfc80 100644 --- a/src/pages/welcome-page.jsx +++ b/src/pages/welcome-page.jsx @@ -1,30 +1,20 @@ -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; import PostList from 'widgets/post-list'; -class WelcomePage extends AsyncComponent { - static displayName = 'WelcomePage'; +async function WelcomePage(props) { + const { wp, route } = props; + const [ show ] = useProgress(); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let props = { route }; - meanwhile.show() - props.posts = await wp.fetchPosts(); - meanwhile.show() - props.medias = await wp.fetchFeaturedMedias(props.posts, 10); - return ; - } -} + render(); + const posts = await wp.fetchPosts(); + render(); + const medias = await wp.fetchFeaturedMedias(posts, 10); + render(); -class WelcomePageSync extends PureComponent { - static displayName = 'WelcomePageSync'; - - render() { - let { route, posts, medias } = this.props; - return ( + function render() { + show(
@@ -32,23 +22,9 @@ class WelcomePageSync extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - WelcomePage.propTypes = { - wp: PropTypes.instanceOf(WordPress), - route: PropTypes.instanceOf(Route), - }; - WelcomePageSync.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), - posts: PropTypes.arrayOf(PropTypes.object), - medias: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - }; -} +const component = Relaks(WelcomePage); export { - WelcomePage as default, - WelcomePage, - WelcomePageSync, + component as default, + component as WelcomePage, }; diff --git a/src/props.js b/src/props.js new file mode 100644 index 0000000..471b1f3 --- /dev/null +++ b/src/props.js @@ -0,0 +1,124 @@ +import * as PropTypes from 'prop-types'; + +import { Route } from 'routing'; +import WordPress from 'wordpress'; + +import ArchivePage from 'pages/archive-page'; +import CategoryPage from 'pages/category-page'; +import PagePage from 'pages/page-page'; +import PostPage from 'pages/post-page'; +import SearchPage from 'pages/search-page'; +import TagPage from 'pages/tag-page'; +import WelcomePage from 'pages/welcome-page'; + +ArchivePage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +CategoryPage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +PagePage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +PostPage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +SearchPage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +TagPage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +WelcomePage.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; + +import Breadcrumb from 'widgets/breadcrumb'; +import CommentListView from 'widgets/comment-list-view'; +import CommentList from 'widgets/comment-list'; +import CommentSection from 'widgets/comment-section'; +import HTML from 'widgets/html'; +import ImageDialog from 'widgets/image-dialog'; +import MediaView from 'widgets/media-view'; +import PageListView from 'widgets/page-list-view'; +import PageList from 'widgets/page-list'; +import PageView from 'widgets/page-view'; +import PostListView from 'widgets/post-list-view'; +import PostList from 'widgets/post-list'; +import PostView from 'widgets/post-view'; +import SideNav from 'widgets/side-nav'; +import TagList from 'widgets/tag-list'; +import TopNav from 'widgets/top-nav'; + +Breadcrumb.propTypes = { + trail: PropTypes.arrayOf(PropTypes.object), +}; +CommentListView.propTypes = { + allComments: PropTypes.arrayOf(PropTypes.object), + comment: PropTypes.object, +}; +CommentList.propTypes = { + allComments: PropTypes.arrayOf(PropTypes.object), + parentCommentID: PropTypes.number, +}; +CommentSection.propTypes = { + comments: PropTypes.arrayOf(PropTypes.object), +}; +HTML.propTypes = { + text: PropTypes.string, + transform: PropTypes.func, +}; +ImageDialog.propTypes = { + imageURL: PropTypes.string, + onClose: PropTypes.func, +}; +MediaView.propTypes = { + media: PropTypes.object, + size: PropTypes.string, +}; +PageListView.propTypes = { + page: PropTypes.object, + route: PropTypes.instanceOf(Route).isRequired, +}; +PageList.propTypes = { + pages: PropTypes.arrayOf(PropTypes.object), + route: PropTypes.instanceOf(Route).isRequired, +}; +PageView.propTypes = { + page: PropTypes.object, + transform: PropTypes.func, +}; +PostList.propTypes = { + posts: PropTypes.arrayOf(PropTypes.object), + medias: PropTypes.arrayOf(PropTypes.object), + route: PropTypes.instanceOf(Route), + minimum: PropTypes.number, + maximum: PropTypes.number, +}; +PostListView.propTypes = { + post: PropTypes.object, + route: PropTypes.instanceOf(Route).isRequired, +}; +PostView.propTypes = { + post: PropTypes.object, + author: PropTypes.object, + transform: PropTypes.func, +}; +SideNav.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; +TagList.propTypes = { + tags: PropTypes.arrayOf(PropTypes.object), +}; +TopNav.propTypes = { + wp: PropTypes.instanceOf(WordPress).isRequired, + route: PropTypes.instanceOf(Route).isRequired, +}; diff --git a/src/widgets/breadcrumb.jsx b/src/widgets/breadcrumb.jsx index cce092b..4cb7a8a 100644 --- a/src/widgets/breadcrumb.jsx +++ b/src/widgets/breadcrumb.jsx @@ -1,27 +1,15 @@ -import React, { PureComponent } from 'react'; +import React from 'react'; -class Breadcrumb extends PureComponent { - static displayName = 'Breadcrumb'; - - render() { - let { trail } = this.props; - let children = [] - let key = 0; - for (let item of trail) { - children.push({item.label}); - children.push(' > '); - } - children.pop(); - return

{children}

; +function Breadcrumb(props) { + const { trail } = props; + const children = [] + let key = 0; + for (let item of trail) { + children.push({item.label}); + children.push(' > '); } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - Breadcrumb.propTypes = { - trail: PropTypes.arrayOf(PropTypes.object), - }; + children.pop(); + return

{children}

; } export { diff --git a/src/widgets/comment-list-view.jsx b/src/widgets/comment-list-view.jsx index da898a3..4b703c7 100644 --- a/src/widgets/comment-list-view.jsx +++ b/src/widgets/comment-list-view.jsx @@ -1,31 +1,27 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; +import React from 'react'; import HTML from 'widgets/html'; import CommentList from 'widgets/comment-list'; -class CommentListView extends PureComponent { - static displayName = 'CommentListView'; +function CommentListView(props) { + const { comment, allComments } = props; + const content = _.get(comment, 'content.rendered', ''); + const avatarURL = _.get(comment, 'author_avatar_urls.24'); + const name = _.get(comment, 'author_name'); - render() { - let { comment } = this.props; - let content = _.get(comment, 'content.rendered', ''); - let avatarURL = _.get(comment, 'author_avatar_urls.24'); - let name = _.get(comment, 'author_name'); - return ( -
-
- - {name}: -
- - {this.renderReplies()} + return ( +
+
+ + {name}:
- ); - } + + {renderReplies()} +
+ ); - renderReplies() { - let { comment, allComments } = this.props; + function renderReplies() { if (!_.some(allComments, { parent: comment.id })) { return null; } @@ -33,19 +29,10 @@ class CommentListView extends PureComponent {
- ) + ); } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - CommentListView.propTypes = { - allComments: PropTypes.arrayOf(PropTypes.object), - comment: PropTypes.object, - }; -} - export { CommentListView as default, CommentListView, diff --git a/src/widgets/comment-list.jsx b/src/widgets/comment-list.jsx index e9c7620..1e3eeb8 100644 --- a/src/widgets/comment-list.jsx +++ b/src/widgets/comment-list.jsx @@ -1,35 +1,23 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; +import React from 'react'; import CommentListView from 'widgets/comment-list-view'; -class CommentList extends PureComponent { - static displayName = 'CommentList' +function CommentList(props) { + const { allComments, parentCommentID } = props; + const comments = _.filter(allComments, { parent: parentCommentID }); - render() { - let { allComments, parentCommentID } = this.props; - let comments = _.filter(allComments, { parent: parentCommentID }); - return ( -
- { - _.map(comments, (comment) => { - return ; - }) - } -
- ) + return ( +
+ {comments.map(renderComment)} +
+ ); + + function renderComment(comment, i) { + return ; } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - CommentList.propTypes = { - allComments: PropTypes.arrayOf(PropTypes.object), - parentCommentID: PropTypes.number, - }; -} - export { CommentList as default, CommentList, diff --git a/src/widgets/comment-section.jsx b/src/widgets/comment-section.jsx index 16a256c..1bbec46 100644 --- a/src/widgets/comment-section.jsx +++ b/src/widgets/comment-section.jsx @@ -1,42 +1,19 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; +import React from 'react'; import CommentList from 'widgets/comment-list'; -class CommentSection extends PureComponent { - static displayName = 'CommentSection'; - - render() { - let { comments } = this.props; - if (_.isEmpty(comments)) { - return null; - } - return ( -
-

Comments

- -
- ); +function CommentSection(props) { + const { comments } = props; + if (_.isEmpty(comments)) { + return null; } - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate(prevProps, prevState) { - let { allComments } = this.props; - if (allComments) { - allComments.more(); - } - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - CommentSection.propTypes = { - comments: PropTypes.arrayOf(PropTypes.object), - }; + return ( +
+

Comments

+ +
+ ); } export { diff --git a/src/widgets/html.jsx b/src/widgets/html.jsx index 2b28429..6fc1138 100644 --- a/src/widgets/html.jsx +++ b/src/widgets/html.jsx @@ -1,22 +1,12 @@ -import React, { PureComponent } from 'react'; +import React from 'react'; import ReactHtmlParser from 'react-html-parser'; -class HTML extends PureComponent { - render() { - let { text, transform } = this.props; - let options = { transform }; - // fix unescaped < - text = text.replace(/<([^>]*)]*) { + if (onClose) { + onClose({ + type: 'close', + target, + }); + } + }; + + const container = document.getElementById('overlay'); + const dialog = renderDialog(); + return ReactDOM.createPortal(dialog, container); + + function renderDialog() { return (
-
+
-
+
@@ -30,25 +37,6 @@ class ImageDialog extends PureComponent {
); } - - handleCloseClick = (evt) => { - let { onClose } = this.props; - if (onClose) { - onClose({ - type: 'close', - target: this, - }); - } - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - ImageDialog.propTypes = { - imageURL: PropTypes.string, - onClose: PropTypes.func, - }; } export { diff --git a/src/widgets/media-view.jsx b/src/widgets/media-view.jsx index 665f462..8f07071 100644 --- a/src/widgets/media-view.jsx +++ b/src/widgets/media-view.jsx @@ -1,38 +1,20 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; +import React from 'react'; -class MediaView extends PureComponent { - static displayName = 'MediaView'; - - render() { - let { media, size } = this.props; - let info = _.get(media, [ 'media_details', 'sizes', size ]); - if (!info) { - info = media; - } - let props = { - src: info.source_url, - width: info.width, - height: info.height, - }; - return ; +function MediaView(props) { + const { media, size } = props; + let info = _.get(media, [ 'media_details', 'sizes', size ]); + if (!info) { + info = media; } + return ; } MediaView.defaultProps = { size: 'thumbnail', }; -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - MediaView.propTypes = { - media: PropTypes.object, - size: PropTypes.string, - }; -} - export { MediaView as default, MediaView, diff --git a/src/widgets/page-list-view.jsx b/src/widgets/page-list-view.jsx index a23ad03..b491307 100644 --- a/src/widgets/page-list-view.jsx +++ b/src/widgets/page-list-view.jsx @@ -1,31 +1,17 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; -import { Route } from 'routing'; +import React from 'react'; import HTML from 'widgets/html'; -class PageListView extends PureComponent { - static displayName = 'PageListView'; - - render() { - let { route, page } = this.props; - let title = _.get(page, 'title.rendered', ''); - let url = route.prefetchObjectURL(page); - return ( -
- -
- ); - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PageListView.propTypes = { - page: PropTypes.object, - route: PropTypes.instanceOf(Route).isRequired, - }; +function PageListView(props) { + const { route, page } = props; + const title = _.get(page, 'title.rendered', ''); + const url = route.prefetchObjectURL(page); + return ( +
+ +
+ ); } export { diff --git a/src/widgets/page-list.jsx b/src/widgets/page-list.jsx index f27ae6b..eeae626 100644 --- a/src/widgets/page-list.jsx +++ b/src/widgets/page-list.jsx @@ -1,51 +1,32 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; +import React, { useEffect } from 'react'; import { Route } from 'routing'; import PageListView from 'widgets/page-list-view'; -class PageList extends PureComponent { - static displayName = 'PageList' +function PageList(props) { + let { route, pages } = props; + if (!pages) { + return null; + } - render() { - let { route, pages } = this.props; - if (!pages) { - return null; - } + useEffect(() => { + pages.more(); + }, [ pages ]); + + return ( +
    + {pages.map(renderPage)} +
+ ); + + function renderPage(page, i) { return ( -
    - { - pages.map((page) => { - return ( -
  • - -
  • - ); - }) - } -
- ) +
  • + +
  • + ); } - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate(prevProps, prevState) { - let { pages } = this.props; - if (pages) { - pages.more(); - } - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PageList.propTypes = { - pages: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route).isRequired, - }; } export { diff --git a/src/widgets/page-view.jsx b/src/widgets/page-view.jsx index 630b81f..b6d0fd6 100644 --- a/src/widgets/page-view.jsx +++ b/src/widgets/page-view.jsx @@ -1,41 +1,27 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; +import React from 'react'; import HTML from 'widgets/html'; -class PageView extends PureComponent { - static displayName = 'PageView'; +function PageView(props) { + const { page, transform } = props; + const title = _.get(page, 'title.rendered', ''); + const content = _.get(page, 'content.rendered', ''); + const modified = _.get(page, 'modified_gmt'); + const date = (modified) ? Moment(modified).format('LL') : ''; - render() { - let { page, transform } = this.props; - let title = _.get(page, 'title.rendered', ''); - let content = _.get(page, 'content.rendered', ''); - let date = _.get(page, 'modified_gmt'); - if (date) { - date = Moment(date).format('LL'); - } - return ( -
    -
    -
    {date}
    -
    -

    -
    - -
    + return ( +
    +
    +
    {date}
    - ); - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PageView.propTypes = { - page: PropTypes.object, - transform: PropTypes.func, - }; +

    +
    + +
    +
    + ); } export { diff --git a/src/widgets/post-list-view.jsx b/src/widgets/post-list-view.jsx index 1e2ac13..670dcae 100644 --- a/src/widgets/post-list-view.jsx +++ b/src/widgets/post-list-view.jsx @@ -1,46 +1,26 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; -import { Route } from 'routing'; +import React from 'react'; import HTML from 'widgets/html'; import MediaView from 'widgets/media-view'; -class PostListView extends PureComponent { - static displayName = 'PostListView'; +function PostListView(props) { + const { route, post, media } = props; + const title = _.get(post, 'title.rendered', ''); + const excerptRendered = _.get(post, 'excerpt.rendered', ''); + const excerpt = cleanExcerpt(excerptRendered); + const url = route.prefetchObjectURL(post); + const published = _.get(post, 'date_gmt'); + const date = (published) ? Moment(published).format('L') : ''; - render() { - let { route, post, media } = this.props; - let title = _.get(post, 'title.rendered', ''); - let excerpt = _.get(post, 'excerpt.rendered', ''); - excerpt = cleanExcerpt(excerpt); - let url = route.prefetchObjectURL(post); - let date = _.get(post, 'date_gmt'); - if (date) { - date = Moment(date).format('L'); - } - if (media) { - return ( -
    -
    - -
    -
    -
    -

    - -

    -
    {date}
    -
    -
    - -
    -
    + if (media) { + return ( +
    +
    +
    - ); - } else { - return ( -
    +

    @@ -51,28 +31,33 @@ class PostListView extends PureComponent {

    - ); +
    + ); + } else { + return ( +
    +
    +

    + +

    +
    {date}
    +
    +
    + +
    +
    + ); + } + + function cleanExcerpt(excerpt) { + const index = excerpt.indexOf('
    - { - posts.map((post) => { - let media = _.find(medias, { id: post.featured_media }); - return - }) - } -
    - ); - } + useEffect(() => { + const handleScroll = (evt) => { + loadMore(0.5); + }; + document.addEventListener('scroll', handleScroll); - componentDidMount() { - document.addEventListener('scroll', this.handleScroll); - this.componentDidUpdate(); - } - - componentDidUpdate(prevProps, prevState) { - let { posts, minimum, maximum } = this.props; - if (posts && posts.length < minimum) { + return () => { + document.removeEventListener('scroll', handleScroll); + }; + }); + useEffect(() => { + if (posts && posts.more && posts.length < minimum) { posts.more(); } else { - // load more records if we're still near the bottom - let { scrollTop, scrollHeight } = document.body.parentNode; - if (scrollTop > scrollHeight * 0.75) { - if (posts && posts.length < maximum) { - posts.more(); - } - } + loadMore(0.75); } + }, [ posts ]); + + if (!posts) { + return null; + } + return ( +
    + {posts.map(renderPost)} +
    + ); + + function renderPost(post, i) { + let media = _.find(medias, { id: post.featured_media }); + return } - componentWillUnmount() { - document.removeEventListener('scroll', this.handleScroll); - } - - handleScroll = (evt) => { - let { posts, maximum } = this.props; - let { scrollTop, scrollHeight } = document.body.parentNode; - if (scrollTop > scrollHeight * 0.5) { - if (posts && posts.length < maximum) { + function loadMore(fraction) { + const { scrollTop, scrollHeight } = document.body.parentNode; + if (scrollTop > scrollHeight * fraction) { + if (posts && posts.more && posts.length < maximum) { posts.more(); } - } + } } } @@ -65,18 +54,6 @@ PostList.defaultProps = { maximum: 500, }; -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PostList.propTypes = { - posts: PropTypes.arrayOf(PropTypes.object), - medias: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route), - minimum: PropTypes.number, - maximum: PropTypes.number, - }; -} - export { PostList as default, PostList, diff --git a/src/widgets/post-view.jsx b/src/widgets/post-view.jsx index ef2d413..846d672 100644 --- a/src/widgets/post-view.jsx +++ b/src/widgets/post-view.jsx @@ -1,52 +1,17 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; import HTML from 'widgets/html'; import ImageDialog from 'widgets/image-dialog'; -class PostView extends PureComponent { - static displayName = 'PostView'; +function PostView(props) { + const { post, author, transform } = props; + const [ imageURL, setImageURL ] = useState(null); - constructor(props) { - super(props); - this.state = { - imageURL: null, - }; - } - - render() { - let { post, author, transform } = this.props; - let title = _.get(post, 'title.rendered', ''); - let content = _.get(post, 'content.rendered', ''); - let date = _.get(post, 'date_gmt'); - let name = _.get(author, 'name', '\u00a0'); - if (date) { - date = Moment(date).format('LL'); - } - return ( -
    -
    -
    {date}
    -
    {name}
    -
    -

    -
    - -
    - {this.renderImageDialog()} -
    - ); - } - - renderImageDialog() { - let { imageURL } = this.state; - return ; - } - - handleClick = (evt) => { - let target = evt.target; - let container = evt.currentTarget; + const handleClick = (evt) => { + const target = evt.target; + const container = evt.currentTarget; if (target.tagName === 'IMG') { let link; for (let p = target; p && p !== container; p = p.parentNode) { @@ -56,26 +21,33 @@ class PostView extends PureComponent { } } if (link) { - let imageURL = link.href; - this.setState({ imageURL }); + setImageURL(link.href); evt.preventDefault(); } } - } - - handleDialogClose = (evt) => { - this.setState({ imageURL: null }); - } -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - PostView.propTypes = { - post: PropTypes.object, - author: PropTypes.object, - transform: PropTypes.func, }; + const handleDialogClose = (evt) => { + setImageURL(null); + }; + + const title = _.get(post, 'title.rendered', ''); + const content = _.get(post, 'content.rendered', ''); + const name = _.get(author, 'name', '\u00a0'); + const published = _.get(post, 'date_gmt'); + const date = (published) ? Moment(published).format('LL') : ''; + return ( +
    +
    +
    {date}
    +
    {name}
    +
    +

    +
    + +
    + +
    + ); } export { diff --git a/src/widgets/side-nav.jsx b/src/widgets/side-nav.jsx index 8e26b40..94c898e 100644 --- a/src/widgets/side-nav.jsx +++ b/src/widgets/side-nav.jsx @@ -1,160 +1,134 @@ import _ from 'lodash'; import Moment from 'moment'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React, { useState } from 'react'; +import Relaks, { useProgress } from 'relaks/hooks'; -class SideNav extends AsyncComponent { - static displayName = 'SideNav'; +async function SideNav(props) { + const { wp, route } = props; + const { date } = route.params; + const [ selectedYear, setSelectedYear ] = useState(() => { + return _.get(date, 'year', Moment().year()); + }); + const [ show ] = useProgress(50, 50); - constructor(props) { - super(props); - let { route } = this.props; - let { date } = route.params; - let selectedYear = _.get(date, 'year', Moment().year()); - this.state = { selectedYear }; - } - - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let { selectedYear } = this.state; - let props = { - route, - selectedYear, - onYearSelect: this.handleYearSelect, - }; - meanwhile.delay(50, 50); - meanwhile.show(); - - // get all categories - props.categories = await wp.fetchCategories(); - meanwhile.show(); - - // get top tags - props.tags = await wp.fetchTopTags(); - meanwhile.show(); - - // get the date range of posts and use that to build the list of - // years and months - let range = await wp.getPostDateRange(); - props.archives = []; - if (range) { - // loop through the years - let lastYear = range.latest.year(); - let firstYear = range.earliest.year(); - for (let y = lastYear; y >= firstYear; y--) { - let yearEntry = { - year: y, - label: Moment(`${y}-01-01`).format('YYYY'), - months: [] - }; - props.archives.push(yearEntry); - - // loop through the months - let lastMonth = (y === lastYear) ? range.latest.month() : 11; - let firstMonth = (y === firstYear) ? range.earliest.month() : 0; - for (let m = lastMonth; m >= firstMonth; m--) { - let start = Moment(new Date(y, m, 1)); - let end = start.clone().endOf('month'); - let monthEntry = { - year: y, - month: m + 1, - label: start.format('MMMM'), - }; - yearEntry.months.push(monthEntry); - } - } - meanwhile.show(); - } - - if (!wp.ssr) { - props.postLists = []; - try { - // load the posts of each month of the selected year - for (let yearEntry of props.archives) { - if (yearEntry.year === selectedYear) { - for (let monthEntry of yearEntry.months) { - let posts = await wp.fetchPostsInMonth(monthEntry); - props.postLists = _.concat(props.postLists, { monthEntry, posts }); - meanwhile.show(); - } - } - } - - // load the posts of each category - for (let category of props.categories) { - if (category.count > 0) { - let posts = await wp.fetchPostsInCategory(category); - props.postLists = _.concat(props.postLists, { category, posts }); - meanwhile.show(); - } - } - - // load the posts of each tag - for (let tag of props.tags) { - if (tag.count > 0) { - let posts = await wp.fetchPostsWithTag(tag); - props.postLists = _.concat(props.postLists, { tag, posts }); - meanwhile.show(); - } - } - } catch (err) { - } - } - return ; - } - - handleYearSelect = (evt) => { - let { selectedYear } = this.state; - if (selectedYear !== evt.year) { - selectedYear = evt.year; + const handleYearClick = (evt) => { + const year = parseInt(evt.currentTarget.getAttribute('data-year')); + if (selectedYear !== year) { + setSelectedYear(year); } else { - selectedYear = NaN; + setSelectedYear(NaN); } - this.setState({ selectedYear }); + }; + const handleMoreTagClick = (evt) => { + tags.more(); + }; + + render(); + // get all categories + const categories = await wp.fetchCategories(); + render (); + // get top tags + const tags = await wp.fetchTopTags(); + render (); + + // get the date range of posts and use that to build the list of + // years and months + const range = await wp.getPostDateRange(); + const archives = []; + if (range) { + // loop through the years + let lastYear = range.latest.year(); + let firstYear = range.earliest.year(); + for (let y = lastYear; y >= firstYear; y--) { + let yearEntry = { + year: y, + label: Moment(`${y}-01-01`).format('YYYY'), + months: [] + }; + archives.push(yearEntry); + + // loop through the months + let lastMonth = (y === lastYear) ? range.latest.month() : 11; + let firstMonth = (y === firstYear) ? range.earliest.month() : 0; + for (let m = lastMonth; m >= firstMonth; m--) { + let start = Moment(new Date(y, m, 1)); + let end = start.clone().endOf('month'); + let monthEntry = { + year: y, + month: m + 1, + label: start.format('MMMM'), + }; + yearEntry.months.push(monthEntry); + } + } + render(); } -} -class SideNavSync extends PureComponent { - static displayName = 'SideNavSync'; + const postLists = []; + if (!wp.ssr) { + try { + // load the posts of each month of the selected year + for (let yearEntry of archives) { + if (yearEntry.year === selectedYear) { + for (let monthEntry of yearEntry.months) { + const posts = await wp.fetchPostsInMonth(monthEntry); + postLists.push({ monthEntry, posts }); + render(); + } + } + } - render() { - return ( + // load the posts of each category + for (let category of categories) { + if (category.count > 0) { + const posts = await wp.fetchPostsInCategory(category); + postLists.push({ category, posts }); + render(); + } + } + + // load the posts of each tag + for (let tag of tags) { + if (tag.count > 0) { + const posts = await wp.fetchPostsWithTag(tag); + postLists.push({ tag, posts }); + render(); + } + } + } catch (err) { + } + } + + function render() { + show(
    - {this.renderCategories()} - {this.renderTags()} - {this.renderArchives()} + {renderCategories()} + {renderTags()} + {renderArchives()}
    - ) + ); } - renderCategories() { - let { categories } = this.props; + function renderCategories() { // only top-level categories - categories = _.filter(categories, { parent: 0 }); + const subcategories = _.filter(categories, { parent: 0 }); // don't show categories with no post - categories = _.filter(categories, 'count'); - categories = _.orderBy(categories, [ 'name' ], [ 'asc' ]); - if (_.isEmpty(categories)) { + const filtered = _.filter(subcategories, 'count'); + const ordered = _.orderBy(filtered, [ 'name' ], [ 'asc' ]); + if (_.isEmpty(ordered)) { return null; } return (

    Categories

      - { - categories.map((category, i) => { - return this.renderCategory(category, i); - }) - } + {ordered.map(renderCategory)}
    ); } - renderCategory(category, i) { - let { route, postLists } = this.props; + function renderCategory(category, i) { let { categorySlug } = route.params; let name = _.get(category, 'name', ''); let description = _.unescape(_.get(category, 'description', '').replace(/'/g, "'")); @@ -171,37 +145,31 @@ class SideNavSync extends PureComponent { return (
  • {name} - {this.renderSubcategories(category)} + {renderSubcategories(category)}
  • ); } - renderTags() { - let { tags } = this.props; + function renderTags() { // don't show tags with no post - tags = _.filter(tags, 'count'); + const activeTags = _.filter(tags, 'count'); // list tags with more posts first - tags = _.orderBy(tags, [ 'count', 'name' ], [ 'desc', 'asc' ]); - if (_.isEmpty(tags)) { + const ordered = _.orderBy(activeTags, [ 'count', 'name' ], [ 'desc', 'asc' ]); + if (_.isEmpty(ordered)) { return null; } return (

    Tags

    - { - tags.map((tag, i) => { - return this.renderTag(tag, i); - }) - } - {this.renderMoreTagButton()} + {ordered.map(renderTag)} + {renderMoreTagButton()}
    ); } - renderTag(tag, i) { - let { route, postLists } = this.props; + function renderTag(tag, i) { let { tagSlug } = route.params; let name = _.get(tag, 'name', ''); let description = _.unescape(_.get(tag, 'description', '').replace(/'/g, "'")); @@ -223,38 +191,31 @@ class SideNavSync extends PureComponent { ); } - renderMoreTagButton() { - let { tags } = this.props; + function renderMoreTagButton() { if (!_.some(tags, 'count')) { return null; } if (!(tags.length < tags.total) || tags.length >= 100) { return null; } - return ... more; + return ... more; } - renderSubcategories(category) { - let { categories } = this.props; - let subcategories = _.filter(categories, { parent: category.id }); - subcategories = _.filter(subcategories, 'count'); - subcategories = _.orderBy(subcategories, [ 'count', 'name' ], [ 'desc', 'asc' ]); - if (_.isEmpty(subcategories)) { + function renderSubcategories(category) { + const subcategories = _.filter(categories, { parent: category.id }); + const filtered = _.filter(subcategories, 'count'); + const ordered = _.orderBy(filtered, [ 'count', 'name' ], [ 'desc', 'asc' ]); + if (_.isEmpty(ordered)) { return null; } return (
      - { - subcategories.map((subcategory, i) => { - return this.renderCategory(subcategory, i); - }) - } + {ordered.map(renderCategory)}
    ); } - renderArchives() { - let { archives } = this.props; + function renderArchives() { if (_.isEmpty(archives)) { return null; } @@ -262,109 +223,68 @@ class SideNavSync extends PureComponent {

    Archives

      - { - archives.map((yearEntry, i) => { - return this.renderYear(yearEntry, i); - }) - } + {archives.map(renderYear)}
    ); } - renderYear(yearEntry, i) { - let { selectedYear } = this.props; - let listClass = 'months'; + function renderYear(yearEntry, i) { + const listClassNames = [ 'months'] ; if (yearEntry.year !== selectedYear) { - listClass += ' collapsed'; + listClassNames.push('collapsed'); } return (
  • - + {yearEntry.label} -
      - { - yearEntry.months.map((entry, i) => { - return this.renderMonth(entry, i); - }) - } +
        + {yearEntry.months.map(renderMonth)}
      ) } - renderMonth(monthEntry, i) { - let { route, postLists, selectedYear } = this.props; - let { date } = route.params; - let className, url; + function renderMonth(monthEntry, i) { + const { date } = route.params; + const classNames = []; + let url; if (monthEntry.year !== selectedYear) { return null; } if (date && monthEntry.year === date.year && monthEntry.month === date.month) { - className = 'selected'; + classNames.push('selected'); } - let postList = _.find(postLists, { monthEntry }); + const postList = _.find(postLists, { monthEntry }); if (!postList || !_.isEmpty(postList.posts)) { url = route.prefetchArchiveURL(monthEntry); } else { - className = 'disabled'; + classNames.push('disabled'); } return (
    • - {monthEntry.label} + + {monthEntry.label} +
    • ); } - handleYearClick = (evt) => { - let { onYearSelect } = this.props; - let year = parseInt(evt.currentTarget.getAttribute('data-year')); - if (onYearSelect) { - onYearSelect({ - type: 'yearselect', - target: this, - year, - }); + function hasRecentPost(postList, daysOld) { + if (!postList || _.isEmpty(postList.posts)) { + return false; } - } - - handleMoreTagClick = (evt) => { - let { tags } = this.props; - tags.more(); + const post = _.first(postList.posts); + const limit = Moment().subtract(daysOld, 'day'); + const publicationDate = Moment(post.date_gmt); + return limit < publicationDate; } } -function hasRecentPost(postList, daysOld) { - if (!postList || _.isEmpty(postList.posts)) { - return false; - } - let post = _.first(postList.posts); - let limit = Moment().subtract(daysOld, 'day'); - let publicationDate = Moment(post.date_gmt); - return limit < publicationDate; -} - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - SideNav.propTypes = { - wp: PropTypes.instanceOf(WordPress).isRequired, - route: PropTypes.instanceOf(Route).isRequired, - }; - SideNavSync.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), - tags: PropTypes.arrayOf(PropTypes.object), - archives: PropTypes.arrayOf(PropTypes.object), - postLists: PropTypes.arrayOf(PropTypes.object), - selectedYear: PropTypes.number, - route: PropTypes.instanceOf(Route), - onYearSelect: PropTypes.func, - }; -} +const component = Relaks(SideNav); export { - SideNav as default, - SideNav, - SideNavSync, + component as default, + component as SideNav, }; diff --git a/src/widgets/tag-list.jsx b/src/widgets/tag-list.jsx index 8a1240f..69c4b3e 100644 --- a/src/widgets/tag-list.jsx +++ b/src/widgets/tag-list.jsx @@ -1,30 +1,20 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; +import React from 'react'; -class TagList extends PureComponent { - static displayName = 'TagList'; - - render() { - let { tags } = this.props; - if (_.isEmpty(tags)) { - return null; - } - return ( -
      - Tags: - { - tags.map((tag, i) => { - return this.renderTag(tag, i); - }) - } -
      - ); +function TagList(props) { + const { route, tags } = props; + if (_.isEmpty(tags)) { + return null; } + return ( +
      + Tags: {tags.map(renderTag)} +
      + ); - renderTag(tag, i) { - let { route } = this.props; - let name = _.get(tag, 'name', ''); - let url = route.prefetchObjectURL(tag); + function renderTag(tag, i) { + const name = _.get(tag, 'name', ''); + const url = route.prefetchObjectURL(tag); return ( {name} @@ -34,14 +24,6 @@ class TagList extends PureComponent { } } -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - TagList.propTypes = { - tags: PropTypes.arrayOf(PropTypes.object), - }; -} - export { TagList as default, TagList, diff --git a/src/widgets/top-nav.jsx b/src/widgets/top-nav.jsx index 63fab2c..8a920ea 100644 --- a/src/widgets/top-nav.jsx +++ b/src/widgets/top-nav.jsx @@ -1,52 +1,46 @@ import _ from 'lodash'; -import React, { PureComponent } from 'react'; -import { AsyncComponent } from 'relaks'; -import { Route } from 'routing'; -import WordPress from 'wordpress'; +import React from 'react'; +import Relaks, { useProgress, useSaveBuffer } from 'relaks/hooks'; -class TopNav extends AsyncComponent { - static displayName = 'TopNav'; +async function TopNav(props) { + const { wp, route } = props; + const [ show ] = useProgress(); + const [ search, setSearch ] = useSaveBuffer(route.params.search, { + delay: 500, + save: (newSearch) => { + const url = route.getSearchURL(newSearch); + const options = { + replace: (route.params.pageType === 'search') + }; + route.change(url); + }, + }); - async renderAsync(meanwhile) { - let { wp, route } = this.props; - let props = { - route, - }; - meanwhile.show(); - props.site = await wp.fetchSite(); - meanwhile.show(); - props.pages = await wp.fetchPages(); - return ; - } -} + const handleSearchChange = (evt) => { + setSearch(evt.target.value); + }; -class TopNavSync extends PureComponent { - static displayName = 'TopNavSync'; + render(); + const site = await wp.fetchSite(); + render(); + const pages = await wp.fetchPages(); + render(); - constructor(props) { - super(props); - let { route } = props; - let { search } = route.params; - this.searchTimeout = 0; - this.state = { search }; - } - - render() { - let { onMouseOver, onMouseOut } = this.props; - return ( -
      - {this.renderTitleBar()} - {this.renderPageLinkBar()} - {this.renderSearchBar()} + function render() { + show( +
      + {renderTitleBar()} + {renderPageLinkBar()} + {renderSearchBar()}
      ); } - renderTitleBar() { - let { route, site } = this.props; - let name = _.get(site, 'name', ''); - let description = _.unescape(_.get(site, 'description', '').replace(/'/g, "'")); - let url = route.getRootURL(); + function renderTitleBar() { + const name = _.get(site, 'name', ''); + const descriptionHTML = _.get(site, 'description', ''); + const description = _.unescape(descriptionHTML.replace(/'/g, "'")); + const url = route.getRootURL(); return (
      @@ -59,25 +53,19 @@ class TopNavSync extends PureComponent { ); } - renderPageLinkBar() { - let { pages } = this.props; - pages = _.filter(pages, { parent: 0 }); - pages = _.sortBy(pages, 'menu_order'); + function renderPageLinkBar() { + let filtered = _.filter(pages, { parent: 0 }); + let ordered = _.sortBy(filtered, 'menu_order'); return (
      - { - pages.map((page, i) => { - return this.renderPageLinkButton(page, i); - }) - } + {ordered.map(renderPageLinkButton)}
      ); } - renderPageLinkButton(page, i) { - let { route } = this.props; - let title = _.get(page, 'title.rendered'); - let url = route.prefetchObjectURL(page); + function renderPageLinkButton(page, i) { + const title = _.get(page, 'title.rendered'); + const url = route.prefetchObjectURL(page); return (
      {title} @@ -85,71 +73,21 @@ class TopNavSync extends PureComponent { ); } - renderSearchBar() { - let { route } = this.props; - let { search } = this.state; + function renderSearchBar() { return (
      - +
      ); } - - performSearch = (evt) => { - let { search } = this.state; - let { route } = this.props; - let url = route.getSearchURL(search); - let options = { - replace: (route.params.pageType === 'search') - }; - route.change(url); - } - - componentDidUpdate(prevProps, prevState) { - let { route } = this.props; - if (prevProps.route !== route) { - let { search } = route.params; - this.setState({ search }); - } - } - - componentWillUnmount() { - clearTimeout(this.searchTimeout); - } - - handleSearchChange = (evt) => { - let search = evt.target.value; - this.setState({ search }); - clearTimeout(this.searchTimeout); - this.searchTimeout = setTimeout(this.performSearch, 500); - } } -TopNavSync.defaultProps = { - site: {}, - pages: [], - search: '', -}; - -if (process.env.NODE_ENV !== 'production') { - const PropTypes = require('prop-types'); - - TopNav.propTypes = { - wp: PropTypes.instanceOf(WordPress).isRequired, - route: PropTypes.instanceOf(Route).isRequired, - }; - TopNavSync.propTypes = { - site: PropTypes.object, - pages: PropTypes.arrayOf(PropTypes.object), - route: PropTypes.instanceOf(Route).isRequired, - }; -} +const component = Relaks(TopNav); export { - TopNav as default, - TopNav, - TopNavSync, + component as default, + component as TopNav, }; diff --git a/webpack.resolve.js b/webpack.resolve.js index d229340..03273fd 100644 --- a/webpack.resolve.js +++ b/webpack.resolve.js @@ -14,7 +14,8 @@ module.exports = function(config) { }) } } - }) + }); + config.resolve.modules.push(Path.resolve('./node_modules')); }; function resolve(type, module) {