1
0
mirror of https://github.com/trambarhq/relaks-wordpress-example.git synced 2025-09-03 05:02:34 +02:00

Migrated all components to hooks.

Moved propTypes to separate file.
This commit is contained in:
Chung Leong
2019-03-19 08:48:26 -04:00
parent e576a16edb
commit da30bc22a6
27 changed files with 863 additions and 1309 deletions

View File

@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import Wordpress from 'wordpress'; import Wordpress from 'wordpress';
import { Route } from 'routing'; import { Route } from 'routing';
import 'style.scss'; import 'style.scss';
@@ -10,154 +10,120 @@ import SideNav from 'widgets/side-nav';
import TopNav from 'widgets/top-nav'; import TopNav from 'widgets/top-nav';
import ErrorBoundary from 'widgets/error-boundary'; import ErrorBoundary from 'widgets/error-boundary';
class FrontEnd extends PureComponent { function FrontEnd(props) {
static displayName = 'FrontEnd'; 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) { useEffect(() => {
super(props); routeManager.addEventListener('change', setRouteChange);
let { routeManager, dataSource } = this.props; dataSource.addEventListener('change', setWPChange);
this.state = {
route: new Route(routeManager, dataSource), return () => {
wp: new Wordpress(dataSource, props.ssr), routeManager.addEventListener('change', setRouteChange);
sideNavCollapsed: true, dataSource.addEventListener('change', setWPChange);
topNavCollapsed: false,
}; };
} });
useEffect(() => {
/** let previousPos = getScrollPos();
* Render the application const handleScroll = (evt) => {
* const currentPos = getScrollPos();
* @return {VNode} const delta = currentPos - previousPos;
*/ if (delta > 0) {
render() { if (!topNavCollapsed) {
let { route, wp } = this.state; // check to see if we have scroll down efficiently, so that
let { topNavCollapsed } = this.state; // hidden the top nav won't reveal white space
let { sideNavCollapsed } = this.state; const pageContainer = document.getElementsByClassName('page-container')[0];
let PageComponent = route.params.module.default; const page = (pageContainer) ? pageContainer.firstChild : null;
let classNames = []; if (page) {
if (topNavCollapsed) { const pageRect = page.getBoundingClientRect();
classNames.push('top-collapsed'); if (pageRect.top <= 40) {
} collapseTopNav(true);
if (sideNavCollapsed) { }
classNames.push('side-collapsed'); } else {
} collapseTopNav(true);
let key = route.url;
return (
<div className={classNames.join(' ')}>
<ErrorBoundary>
<SideNav route={route} wp={wp} />
<TopNav route={route} wp={wp} />
<div className="page-container">
<PageComponent route={route} wp={wp} key={key} />
</div>
</ErrorBoundary>
<div id="overlay" />
</div>
);
}
/**
* 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 });
} }
} else { }
this.setState({ topNavCollapsed: true }); } else if (delta < -10) {
if (topNavCollapsed) {
collapseTopNav(false);
} }
} }
} else { previousPos = currentPos;
if (topNavCollapsed) { };
this.setState({ topNavCollapsed: false }); 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 (
<div className={classNames.join(' ')}>
<ErrorBoundary>
<SideNav route={route} wp={wp} />
<TopNav route={route} wp={wp} />
<div className="page-container">
<PageComponent route={route} wp={wp} key={key} />
</div>
</ErrorBoundary>
<div id="overlay" />
</div>
);
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) => { function resetScrollPos() {
let { sideNavCollapsed } = this.state; if (document.body.parentElement.scrollTop > 0) {
if (!sideNavCollapsed) { document.body.parentElement.scrollTop = 0;
this.setState({ sideNavCollapsed: true }); } else if (document.body.scrollTop > 0) {
} document.body.scrollTop = 0;
}
handleSwipeRight = (evt) => {
let { sideNavCollapsed } = this.state;
if (sideNavCollapsed) {
this.setState({ sideNavCollapsed: false });
} }
} }
} }
@@ -166,3 +132,7 @@ export {
FrontEnd as default, FrontEnd as default,
FrontEnd FrontEnd
}; };
if (process.env.NODE_ENV !== 'production') {
require('./props');
}

View File

@@ -6,50 +6,50 @@ import { Route, routes } from 'routing';
import WordpressDataSource from 'relaks-wordpress-data-source'; import WordpressDataSource from 'relaks-wordpress-data-source';
import RouteManager from 'relaks-route-manager'; import RouteManager from 'relaks-route-manager';
import { harvest } from 'relaks-harvest'; import { harvest } from 'relaks-harvest';
import Relaks, { plant } from 'relaks'; import { plant } from 'relaks/hooks';
if (process.env.TARGET === 'browser') { if (process.env.TARGET === 'browser') {
async function initialize(evt) { async function initialize(evt) {
// create data source // create data source
let host = process.env.DATA_HOST || `${location.protocol}//${location.host}`; const host = process.env.DATA_HOST || `${location.protocol}//${location.host}`;
let basePath = process.env.BASE_PATH; const basePath = process.env.BASE_PATH;
let dataSource = new WordpressDataSource({ const dataSource = new WordpressDataSource({
baseURL: host + basePath + 'json', baseURL: host + basePath + 'json',
}); });
dataSource.activate(); dataSource.activate();
// create route manager // create route manager
let routeManager = new RouteManager({ const routeManager = new RouteManager({
routes, routes,
basePath, basePath,
useHashFallback: (location.protocol !== 'http:' && location.protocol !== 'https:'), useHashFallback: (location.protocol !== 'http:' && location.protocol !== 'https:'),
}); });
routeManager.addEventListener('beforechange', (evt) => { routeManager.addEventListener('beforechange', (evt) => {
let route = new Route(routeManager, dataSource); const route = new Route(routeManager, dataSource);
evt.postponeDefault(route.setParameters(evt, true)); evt.postponeDefault(route.setParameters(evt, true));
}); });
routeManager.activate(); routeManager.activate();
await routeManager.start(); await routeManager.start();
let container = document.getElementById('react-container'); const container = document.getElementById('react-container');
if (!process.env.DATA_HOST) { if (!process.env.DATA_HOST) {
// there is SSR support when we're fetching data from the same host // there is SSR support when we're fetching data from the same host
// as the HTML page // as the HTML page
let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' }); const ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
let seeds = await harvest(ssrElement, { seeds: true }); const seeds = await harvest(ssrElement, { seeds: true });
plant(seeds); plant(seeds);
hydrate(ssrElement, container); hydrate(ssrElement, container);
} }
let csrElement = createElement(FrontEnd, { dataSource, routeManager }); const csrElement = createElement(FrontEnd, { dataSource, routeManager });
render(csrElement, container); render(csrElement, container);
// check for changes periodically // check for changes periodically
let mtimeURL = host + basePath + '.mtime'; const mtimeURL = host + basePath + '.mtime';
let mtimeLast; let mtimeLast;
for (;;) { for (;;) {
try { try {
let res = await fetch(mtimeURL); const res = await fetch(mtimeURL);
let mtime = await res.text(); const mtime = await res.text();
if (mtime !== mtimeLast) { if (mtime !== mtimeLast) {
if (mtimeLast) { if (mtimeLast) {
dataSource.invalidate(); dataSource.invalidate();
@@ -65,28 +65,28 @@ if (process.env.TARGET === 'browser') {
window.addEventListener('load', initialize); window.addEventListener('load', initialize);
} else if (process.env.TARGET === 'node') { } else if (process.env.TARGET === 'node') {
async function serverSideRender(options) { async function serverSideRender(options) {
let basePath = process.env.BASE_PATH; const basePath = process.env.BASE_PATH;
let dataSource = new WordpressDataSource({ const dataSource = new WordpressDataSource({
baseURL: options.host + basePath + 'json', baseURL: options.host + basePath + 'json',
fetchFunc: options.fetch, fetchFunc: options.fetch,
}); });
dataSource.activate(); dataSource.activate();
let routeManager = new RouteManager({ const routeManager = new RouteManager({
routes, routes,
basePath, basePath,
}); });
routeManager.addEventListener('beforechange', (evt) => { routeManager.addEventListener('beforechange', (evt) => {
let route = new Route(routeManager, dataSource); const route = new Route(routeManager, dataSource);
evt.postponeDefault(route.setParameters(evt, false)); evt.postponeDefault(route.setParameters(evt, false));
}); });
routeManager.activate(); routeManager.activate();
await routeManager.start(options.path); 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); return harvest(ssrElement);
} }
exports.render = serverSideRender; exports.render = serverSideRender;
exports.basePath = process.env.BASE_PATH; exports.basePath = process.env.BASE_PATH;
} }

View File

@@ -1,35 +1,24 @@
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PostList from 'widgets/post-list'; import PostList from 'widgets/post-list';
class ArchivePage extends AsyncComponent { async function ArchivePage(props) {
static displayName = 'ArchivePage'; const { wp, route } = props;
const { date } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const posts = await wp.fetchPostsInMonth(date);
let { date } = route.params; render();
let props = { route };
meanwhile.show(<ArchivePageSync {...props} />);
props.posts = await wp.fetchPostsInMonth(date);
return <ArchivePageSync {...props} />;
}
}
class ArchivePageSync extends PureComponent { function render() {
static displayName = 'ArchivePageSync'; const month = Moment(new Date(date.year, date.month - 1, 1));
const monthLabel = month.format('MMMM YYYY');
render() { const trail = [ { label: 'Archives' }, { label: monthLabel } ];
let { route, posts } = this.props; show(
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 (
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={100} /> <PostList route={route} posts={posts} minimum={100} />
@@ -38,22 +27,9 @@ class ArchivePageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(ArchivePage);
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),
};
}
export { export {
ArchivePage as default, component as default,
ArchivePage, component as ArchivePage,
ArchivePageSync,
}; };

View File

@@ -1,44 +1,34 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PostList from 'widgets/post-list'; import PostList from 'widgets/post-list';
class CategoryPage extends AsyncComponent { async function CategoryPage(props) {
static displayName = 'CategoryPage'; const { wp, route } = props;
const { categorySlug } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const category = await wp.fetchCategory(categorySlug);
let { categorySlug } = route.params; const parentCategories = await wp.fetchParentCategories(category);
let props = { route }; render();
meanwhile.show(<CategoryPageSync {...props} />); const posts = await wp.fetchPostsInCategory(category);
props.category = await wp.fetchCategory(categorySlug); render();
props.parentCategories = await wp.fetchParentCategories(props.category);
meanwhile.show(<CategoryPageSync {...props} />);
props.posts = await wp.fetchPostsInCategory(props.category);
return <CategoryPageSync {...props} />;
}
}
class CategoryPageSync extends PureComponent { function render() {
static displayName = 'CategoryPageSync'; const trail = [ { label: 'Categories' } ];
const categoryLabel = _.get(category, 'name', '');
render() {
let { route, posts, category, parentCategories } = this.props;
let trail = [ { label: 'Categories' } ];
let categoryLabel = _.get(category, 'name', '');
if (parentCategories) { if (parentCategories) {
for (let parentCategory of parentCategories) { for (let parentCategory of parentCategories) {
let label = _.get(parentCategory, 'name', ''); const label = _.get(parentCategory, 'name', '');
let url = route.prefetchObjectURL(parentCategory); const url = route.prefetchObjectURL(parentCategory);
trail.push({ label, url }); trail.push({ label, url });
} }
trail.push({ label: categoryLabel }); trail.push({ label: categoryLabel });
} }
return ( show(
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} /> <PostList route={route} posts={posts} minimum={40} />
@@ -47,23 +37,9 @@ class CategoryPageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(CategoryPage);
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),
};
}
export { export {
CategoryPage as default, component as default,
CategoryPage, component as CategoryPage,
CategoryPageSync,
}; };

View File

@@ -1,44 +1,34 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PageView from 'widgets/page-view'; import PageView from 'widgets/page-view';
import PageList from 'widgets/page-list'; import PageList from 'widgets/page-list';
class PagePage extends AsyncComponent { async function PagePage(props) {
static displayName = 'PagePage'; const { wp, route } = props;
const { pageSlug } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const page = await wp.fetchPage(pageSlug);
let { pageSlug } = route.params; const parentPages = await wp.fetchParentPages(page);
let props = { route }; render();
meanwhile.show(<PagePageSync {...props} />); const childPages = await wp.fetchChildPages(page);
props.page = await wp.fetchPage(pageSlug); render();
props.parentPages = await wp.fetchParentPages(props.page);
meanwhile.show(<PagePageSync {...props} />);
props.childPages = await wp.fetchChildPages(props.page);
return <PagePageSync {...props} />;
}
}
class PagePageSync extends PureComponent { function render() {
static displayName = 'PagePageSync'; const trail = [];
render() {
let { route, page, parentPages, childPages } = this.props;
let trail = [];
if (parentPages) { if (parentPages) {
for (let parentPage of parentPages) { for (let parentPage of parentPages) {
let title = _.get(parentPage, 'title.rendered', ''); const title = _.get(parentPage, 'title.rendered', '');
let url = route.prefetchObjectURL(parentPage); const url = route.prefetchObjectURL(parentPage);
trail.push({ label: <HTML text={title} />, url }) trail.push({ label: <HTML text={title} />, url })
} }
} }
return ( show(
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PageView page={page} transform={route.transformNode} /> <PageView page={page} transform={route.transformNode} />
@@ -48,23 +38,9 @@ class PagePageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(PagePage);
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,
};
}
export { export {
PagePage as default, component as default,
PagePage, component as PagePage,
PagePageSync,
}; };

View File

@@ -1,46 +1,59 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PostView from 'widgets/post-view'; import PostView from 'widgets/post-view';
import TagList from 'widgets/tag-list'; import TagList from 'widgets/tag-list';
import CommentSection from 'widgets/comment-section'; import CommentSection from 'widgets/comment-section';
class PostPage extends AsyncComponent { async function PostPage(props) {
static displayName = 'PostPage'; const { wp, route } = props;
const { postSlug } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const post = await wp.fetchPost(postSlug);
let { postSlug } = route.params; render();
let props = { route }; const categories = await findCategoryChain(post);
meanwhile.show(<PostPageSync {...props} />); render();
props.post = await wp.fetchPost(postSlug); const author = await wp.fetchAuthor(post);
meanwhile.show(<PostPageSync {...props} />); render();
props.categories = await this.findCategoryChain(props.post); const tags = await wp.fetchTagsOfPost(post);
meanwhile.show(<PostPageSync {...props} />); render()
props.author = await wp.fetchAuthor(props.post); let comments;
meanwhile.show(<PostPageSync {...props} />); if (!wp.ssr) {
props.tags = await wp.fetchTagsOfPost(props.post); comments = await wp.fetchComments(post);
if (!wp.ssr) { render();
meanwhile.show(<PostPageSync {...props} />);
props.comments = await wp.fetchComments(props.post);
}
return <PostPageSync {...props} />;
} }
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(
<div className="page">
<Breadcrumb trail={trail} />
<PostView post={post} author={author} transform={route.transformNode} />
<TagList route={route} tags={tags} />
<CommentSection comments={comments} />
</div>
);
}
async function findCategoryChain(post) {
if (!post) return []; if (!post) return [];
let ids = post.categories; const allCategories = await wp.fetchCategories();
let { wp, route } = this.props;
let allCategories = await wp.fetchCategories();
// add categories, including their parents as well // add categories, including their parents as well
let applicable = []; const applicable = [];
let include = (id) => { const include = (id) => {
let category = _.find(allCategories, { id }) let category = _.find(allCategories, { id })
if (category) { if (category) {
if (!_.includes(applicable, category)) { if (!_.includes(applicable, category)) {
@@ -50,20 +63,20 @@ class PostPage extends AsyncComponent {
include(category.parent); include(category.parent);
} }
}; };
for (let id of ids) { for (let id of post.categories) {
include(id); include(id);
} }
// see how recently a category was visited // see how recently a category was visited
let historyIndex = (category) => { const historyIndex = (category) => {
let predicate = { params: { categorySlug: category.slug }}; const predicate = { params: { categorySlug: category.slug }};
return _.findLastIndex(route.history, predicate); return _.findLastIndex(route.history, predicate);
}; };
// see how deep a category is // see how deep a category is
let depth = (category) => { const depth = (category) => {
if (category.parent) { if (category.parent) {
let predicate = { id: category.parent }; const predicate = { id: category.parent };
let parent = _.find(allCategories, predicate); const parent = _.find(allCategories, predicate);
if (parent) { if (parent) {
return depth(parent) + 1; return depth(parent) + 1;
} }
@@ -74,10 +87,10 @@ class PostPage extends AsyncComponent {
// order applicable categories based on how recently it was visited, // order applicable categories based on how recently it was visited,
// how deep it is, and alphabetically; the first criteria makes our // how deep it is, and alphabetically; the first criteria makes our
// breadcrumb works more sensibly // breadcrumb works more sensibly
applicable = _.orderBy(applicable, [ historyIndex, depth, 'name' ], [ 'desc', 'desc', 'asc' ]); const ordered = _.orderBy(applicable, [ historyIndex, depth, 'name' ], [ 'desc', 'desc', 'asc' ]);
let anchorCategory = _.first(applicable); const anchorCategory = _.first(ordered);
let trail = []; const trail = [];
if (anchorCategory) { if (anchorCategory) {
// add category and its ancestors // add category and its ancestors
for (let c = anchorCategory; c; c = _.find(applicable, { id: c.parent })) { for (let c = anchorCategory; c; c = _.find(applicable, { id: c.parent })) {
@@ -94,47 +107,9 @@ class PostPage extends AsyncComponent {
} }
} }
class PostPageSync extends PureComponent { const component = Relaks(PostPage);
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 (
<div className="page">
<Breadcrumb trail={trail} />
<PostView post={post} author={author} transform={route.transformNode} />
<TagList route={route} tags={tags} />
<CommentSection comments={comments} />
</div>
);
}
}
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),
};
}
export { export {
PostPage as default, component as default,
PostPage, component as PostPage,
PostPageSync,
}; };

View File

@@ -1,42 +1,31 @@
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PostList from 'widgets/post-list'; import PostList from 'widgets/post-list';
class SearchPage extends AsyncComponent { async function SearchPage(props) {
static displayName = 'SearchPage'; const { wp, route } = props;
const { search } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const posts = await wp.fetchMatchingPosts(search);
let { search } = route.params; render();
let props = { route };
meanwhile.show(<SearchPageSync {...props} />);
props.posts = await wp.fetchMatchingPosts(search);
return <SearchPageSync {...props} />;
}
}
class SearchPageSync extends PureComponent { function render() {
static displayName = 'SearchPageSync'; const trail = [ { label: 'Search' } ];
render() {
let { route, posts } = this.props;
let { search } = route.params;
let trail = [ { label: 'Search' } ];
if (posts) { if (posts) {
let count = posts.total; const count = posts.total;
if (typeof(count) === 'number') { if (typeof(count) === 'number') {
let s = (count === 1) ? '' : 's'; const s = (count === 1) ? '' : 's';
let msg = `${count} matching article${s}`; const msg = `${count} matching article${s}`;
trail.push({ label: msg }); trail.push({ label: msg });
} }
} else { } else {
trail.push({ label: '...' }); trail.push({ label: '...' });
} }
return ( show(
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} maximum={1000} /> <PostList route={route} posts={posts} minimum={40} maximum={1000} />
@@ -45,22 +34,9 @@ class SearchPageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(SearchPage);
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),
};
}
export { export {
SearchPage as default, component as default,
SearchPage, component as SearchPage,
SearchPageSync,
}; };

View File

@@ -1,35 +1,25 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import Breadcrumb from 'widgets/breadcrumb'; import Breadcrumb from 'widgets/breadcrumb';
import PostList from 'widgets/post-list'; import PostList from 'widgets/post-list';
class TagPage extends AsyncComponent { async function TagPage(props) {
static displayName = 'TagPage'; const { wp, route } = props;
const { tagSlug } = route.params;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const tag = await wp.fetchTag(tagSlug);
let { tagSlug } = route.params; render();
let props = { route }; const posts = await wp.fetchPostsWithTag(tag);
meanwhile.show(<TagPageSync {...props} />); render();
props.tag = await wp.fetchTag(tagSlug);
meanwhile.show(<TagPageSync {...props} />);
props.posts = await wp.fetchPostsWithTag(props.tag);
return <TagPageSync {...props} />;
}
}
class TagPageSync extends PureComponent { function render() {
static displayName = 'TagPageSync'; const tagLabel = _.get(tag, 'name', '');
const trail = [ { label: 'Tags' }, { label: tagLabel } ];
render() { show(
let { route, posts, tag } = this.props;
let tagLabel = _.get(tag, 'name', '');
let trail = [ { label: 'Tags' }, { label: tagLabel } ];
return (
<div className="page"> <div className="page">
<Breadcrumb trail={trail} /> <Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} /> <PostList route={route} posts={posts} minimum={40} />
@@ -38,22 +28,9 @@ class TagPageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(TagPage);
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),
};
}
export { export {
TagPage as default, component as default,
TagPage, component as TagPage,
TagPageSync,
}; };

View File

@@ -1,30 +1,20 @@
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
import PostList from 'widgets/post-list'; import PostList from 'widgets/post-list';
class WelcomePage extends AsyncComponent { async function WelcomePage(props) {
static displayName = 'WelcomePage'; const { wp, route } = props;
const [ show ] = useProgress();
async renderAsync(meanwhile) { render();
let { wp, route } = this.props; const posts = await wp.fetchPosts();
let props = { route }; render();
meanwhile.show(<WelcomePageSync {...props} />) const medias = await wp.fetchFeaturedMedias(posts, 10);
props.posts = await wp.fetchPosts(); render();
meanwhile.show(<WelcomePageSync {...props} />)
props.medias = await wp.fetchFeaturedMedias(props.posts, 10);
return <WelcomePageSync {...props} />;
}
}
class WelcomePageSync extends PureComponent { function render() {
static displayName = 'WelcomePageSync'; show(
render() {
let { route, posts, medias } = this.props;
return (
<div className="page"> <div className="page">
<PostList route={route} posts={posts} medias={medias} minimum={40} /> <PostList route={route} posts={posts} medias={medias} minimum={40} />
</div> </div>
@@ -32,23 +22,9 @@ class WelcomePageSync extends PureComponent {
} }
} }
if (process.env.NODE_ENV !== 'production') { const component = Relaks(WelcomePage);
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),
};
}
export { export {
WelcomePage as default, component as default,
WelcomePage, component as WelcomePage,
WelcomePageSync,
}; };

124
src/props.js Normal file
View File

@@ -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,
};

View File

@@ -1,27 +1,15 @@
import React, { PureComponent } from 'react'; import React from 'react';
class Breadcrumb extends PureComponent { function Breadcrumb(props) {
static displayName = 'Breadcrumb'; const { trail } = props;
const children = []
render() { let key = 0;
let { trail } = this.props; for (let item of trail) {
let children = [] children.push(<a key={key++} href={item.url}>{item.label}</a>);
let key = 0; children.push(' > ');
for (let item of trail) {
children.push(<a key={key++} href={item.url}>{item.label}</a>);
children.push(' > ');
}
children.pop();
return <h4 className="breadcrumb">{children}</h4>;
} }
} children.pop();
return <h4 className="breadcrumb">{children}</h4>;
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
Breadcrumb.propTypes = {
trail: PropTypes.arrayOf(PropTypes.object),
};
} }
export { export {

View File

@@ -1,31 +1,27 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
import CommentList from 'widgets/comment-list'; import CommentList from 'widgets/comment-list';
class CommentListView extends PureComponent { function CommentListView(props) {
static displayName = 'CommentListView'; 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() { return (
let { comment } = this.props; <div className="comment-list-view">
let content = _.get(comment, 'content.rendered', ''); <div className="commenter">
let avatarURL = _.get(comment, 'author_avatar_urls.24'); <img className="avatar" src={avatarURL} />
let name = _.get(comment, 'author_name'); <span className="name">{name}:</span>
return (
<div className="comment-list-view">
<div className="commenter">
<img className="avatar" src={avatarURL} />
<span className="name">{name}:</span>
</div>
<HTML text={content} />
{this.renderReplies()}
</div> </div>
); <HTML text={content} />
} {renderReplies()}
</div>
);
renderReplies() { function renderReplies() {
let { comment, allComments } = this.props;
if (!_.some(allComments, { parent: comment.id })) { if (!_.some(allComments, { parent: comment.id })) {
return null; return null;
} }
@@ -33,19 +29,10 @@ class CommentListView extends PureComponent {
<div className="replies"> <div className="replies">
<CommentList allComments={allComments} parentCommentID={comment.id} /> <CommentList allComments={allComments} parentCommentID={comment.id} />
</div> </div>
) );
} }
} }
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
CommentListView.propTypes = {
allComments: PropTypes.arrayOf(PropTypes.object),
comment: PropTypes.object,
};
}
export { export {
CommentListView as default, CommentListView as default,
CommentListView, CommentListView,

View File

@@ -1,35 +1,23 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import CommentListView from 'widgets/comment-list-view'; import CommentListView from 'widgets/comment-list-view';
class CommentList extends PureComponent { function CommentList(props) {
static displayName = 'CommentList' const { allComments, parentCommentID } = props;
const comments = _.filter(allComments, { parent: parentCommentID });
render() { return (
let { allComments, parentCommentID } = this.props; <div className="comments">
let comments = _.filter(allComments, { parent: parentCommentID }); {comments.map(renderComment)}
return ( </div>
<div className="comments"> );
{
_.map(comments, (comment) => { function renderComment(comment, i) {
return <CommentListView comment={comment} allComments={allComments} key={comment.id} />; return <CommentListView comment={comment} allComments={allComments} key={comment.id} />;
})
}
</div>
)
} }
} }
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
CommentList.propTypes = {
allComments: PropTypes.arrayOf(PropTypes.object),
parentCommentID: PropTypes.number,
};
}
export { export {
CommentList as default, CommentList as default,
CommentList, CommentList,

View File

@@ -1,42 +1,19 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import CommentList from 'widgets/comment-list'; import CommentList from 'widgets/comment-list';
class CommentSection extends PureComponent { function CommentSection(props) {
static displayName = 'CommentSection'; const { comments } = props;
if (_.isEmpty(comments)) {
render() { return null;
let { comments } = this.props;
if (_.isEmpty(comments)) {
return null;
}
return (
<div className="comment-section">
<h3>Comments</h3>
<CommentList allComments={comments} parentCommentID={0} />
</div>
);
} }
return (
componentDidMount() { <div className="comment-section">
this.componentDidUpdate(); <h3>Comments</h3>
} <CommentList allComments={comments} parentCommentID={0} />
</div>
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),
};
} }
export { export {

View File

@@ -1,22 +1,12 @@
import React, { PureComponent } from 'react'; import React from 'react';
import ReactHtmlParser from 'react-html-parser'; import ReactHtmlParser from 'react-html-parser';
class HTML extends PureComponent { function HTML(props) {
render() { const { text, transform } = props;
let { text, transform } = this.props; const options = { transform };
let options = { transform }; // fix unescaped <
// fix unescaped < const fixed = text.replace(/<([^>]*)</g, '&lt;$1<');
text = text.replace(/<([^>]*)</g, '&lt;$1<'); return ReactHtmlParser(fixed, options);
return ReactHtmlParser(text, options);
}
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
HTML.propTypes = {
text: PropTypes.string,
transform: PropTypes.func,
};
} }
export { export {

View File

@@ -1,27 +1,34 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
class ImageDialog extends PureComponent { function ImageDialog(props) {
const { imageURL, onClose } = props;
render() { const target = { func: ImageDialog, props };
let { imageURL } = this.props; if (!imageURL) {
if (!imageURL) { return null;
return null;
}
let container = document.getElementById('overlay');
let dialog = this.renderDialog();
return ReactDOM.createPortal(dialog, container);
} }
renderDialog() { const handleCloseClick = (evt) => {
let { imageURL } = this.props; if (onClose) {
onClose({
type: 'close',
target,
});
}
};
const container = document.getElementById('overlay');
const dialog = renderDialog();
return ReactDOM.createPortal(dialog, container);
function renderDialog() {
return ( return (
<div className="image-dialog"> <div className="image-dialog">
<div className="background" onClick={this.handleCloseClick}/> <div className="background" onClick={handleCloseClick}/>
<div className="foreground"> <div className="foreground">
<div className="box"> <div className="box">
<div className="close-button" onClick={this.handleCloseClick}> <div className="close-button" onClick={handleCloseClick}>
<i className="fa fa-times" /> <i className="fa fa-times" />
</div> </div>
<img className="image" src={imageURL} /> <img className="image" src={imageURL} />
@@ -30,25 +37,6 @@ class ImageDialog extends PureComponent {
</div> </div>
); );
} }
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 { export {

View File

@@ -1,38 +1,20 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React from 'react';
class MediaView extends PureComponent { function MediaView(props) {
static displayName = 'MediaView'; const { media, size } = props;
let info = _.get(media, [ 'media_details', 'sizes', size ]);
render() { if (!info) {
let { media, size } = this.props; info = media;
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 <img {...props} />;
} }
return <img src={info.source_url} width={info.width} height={info.height} />;
} }
MediaView.defaultProps = { MediaView.defaultProps = {
size: 'thumbnail', size: 'thumbnail',
}; };
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
MediaView.propTypes = {
media: PropTypes.object,
size: PropTypes.string,
};
}
export { export {
MediaView as default, MediaView as default,
MediaView, MediaView,

View File

@@ -1,31 +1,17 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import { Route } from 'routing';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
class PageListView extends PureComponent { function PageListView(props) {
static displayName = 'PageListView'; const { route, page } = props;
const title = _.get(page, 'title.rendered', '');
render() { const url = route.prefetchObjectURL(page);
let { route, page } = this.props; return (
let title = _.get(page, 'title.rendered', ''); <div className="page-list-view">
let url = route.prefetchObjectURL(page); <a href={url}><HTML text={title} /></a>
return ( </div>
<div className="page-list-view"> );
<a href={url}><HTML text={title} /></a>
</div>
);
}
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
PageListView.propTypes = {
page: PropTypes.object,
route: PropTypes.instanceOf(Route).isRequired,
};
} }
export { export {

View File

@@ -1,51 +1,32 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { useEffect } from 'react';
import { Route } from 'routing'; import { Route } from 'routing';
import PageListView from 'widgets/page-list-view'; import PageListView from 'widgets/page-list-view';
class PageList extends PureComponent { function PageList(props) {
static displayName = 'PageList' let { route, pages } = props;
if (!pages) {
return null;
}
render() { useEffect(() => {
let { route, pages } = this.props; pages.more();
if (!pages) { }, [ pages ]);
return null;
} return (
<ul className="pages">
{pages.map(renderPage)}
</ul>
);
function renderPage(page, i) {
return ( return (
<ul className="pages"> <li key={page.id}>
{ <PageListView route={route} page={page} />
pages.map((page) => { </li>
return ( );
<li key={page.id}>
<PageListView route={route} page={page} />
</li>
);
})
}
</ul>
)
} }
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 { export {

View File

@@ -1,41 +1,27 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React from 'react';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
class PageView extends PureComponent { function PageView(props) {
static displayName = 'PageView'; 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() { return (
let { page, transform } = this.props; <div className="page">
let title = _.get(page, 'title.rendered', ''); <div className="meta">
let content = _.get(page, 'content.rendered', ''); <div className="date">{date}</div>
let date = _.get(page, 'modified_gmt');
if (date) {
date = Moment(date).format('LL');
}
return (
<div className="page">
<div className="meta">
<div className="date">{date}</div>
</div>
<h1><HTML text={title} /></h1>
<div className="content">
<HTML text={content} transform={transform}/>
</div>
</div> </div>
); <h1><HTML text={title} /></h1>
} <div className="content">
} <HTML text={content} transform={transform}/>
</div>
if (process.env.NODE_ENV !== 'production') { </div>
const PropTypes = require('prop-types'); );
PageView.propTypes = {
page: PropTypes.object,
transform: PropTypes.func,
};
} }
export { export {

View File

@@ -1,46 +1,26 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React from 'react';
import { Route } from 'routing';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
import MediaView from 'widgets/media-view'; import MediaView from 'widgets/media-view';
class PostListView extends PureComponent { function PostListView(props) {
static displayName = 'PostListView'; 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() { if (media) {
let { route, post, media } = this.props; return (
let title = _.get(post, 'title.rendered', ''); <div className="post-list-view with-media">
let excerpt = _.get(post, 'excerpt.rendered', ''); <div className="media">
excerpt = cleanExcerpt(excerpt); <MediaView media={media} />
let url = route.prefetchObjectURL(post);
let date = _.get(post, 'date_gmt');
if (date) {
date = Moment(date).format('L');
}
if (media) {
return (
<div className="post-list-view with-media">
<div className="media">
<MediaView media={media} />
</div>
<div className="text">
<div className="headline">
<h3 className="title">
<a href={url}><HTML text={title} /></a>
</h3>
<div className="date">{date}</div>
</div>
<div className="excerpt">
<HTML text={excerpt} />
</div>
</div>
</div> </div>
); <div className="text">
} else {
return (
<div className="post-list-view">
<div className="headline"> <div className="headline">
<h3 className="title"> <h3 className="title">
<a href={url}><HTML text={title} /></a> <a href={url}><HTML text={title} /></a>
@@ -51,28 +31,33 @@ class PostListView extends PureComponent {
<HTML text={excerpt} /> <HTML text={excerpt} />
</div> </div>
</div> </div>
); </div>
);
} else {
return (
<div className="post-list-view">
<div className="headline">
<h3 className="title">
<a href={url}><HTML text={title} /></a>
</h3>
<div className="date">{date}</div>
</div>
<div className="excerpt">
<HTML text={excerpt} />
</div>
</div>
);
}
function cleanExcerpt(excerpt) {
const index = excerpt.indexOf('<p class="link-more">');
if (index !== -1) {
excerpt = excerpt.substr(0, index);
} }
return excerpt;
} }
} }
function cleanExcerpt(excerpt) {
let index = excerpt.indexOf('<p class="link-more">');
if (index !== -1) {
excerpt = excerpt.substr(0, index);
}
return excerpt;
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
PostListView.propTypes = {
post: PropTypes.object,
route: PropTypes.instanceOf(Route).isRequired,
};
}
export { export {
PostListView as default, PostListView as default,
PostListView, PostListView,

View File

@@ -1,62 +1,51 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React, { useEffect } from 'react';
import { Route } from 'routing';
import PostListView from 'widgets/post-list-view'; import PostListView from 'widgets/post-list-view';
class PostList extends PureComponent { function PostList(props) {
static displayName = 'PostList' const { route, posts, medias, minimum, maximum } = props;
render() { useEffect(() => {
let { route, posts, medias } = this.props; const handleScroll = (evt) => {
if (!posts) { loadMore(0.5);
return null; };
} document.addEventListener('scroll', handleScroll);
return (
<div className="posts">
{
posts.map((post) => {
let media = _.find(medias, { id: post.featured_media });
return <PostListView route={route} post={post} media={media} key={post.id} />
})
}
</div>
);
}
componentDidMount() { return () => {
document.addEventListener('scroll', this.handleScroll); document.removeEventListener('scroll', handleScroll);
this.componentDidUpdate(); };
} });
useEffect(() => {
componentDidUpdate(prevProps, prevState) { if (posts && posts.more && posts.length < minimum) {
let { posts, minimum, maximum } = this.props;
if (posts && posts.length < minimum) {
posts.more(); posts.more();
} else { } else {
// load more records if we're still near the bottom loadMore(0.75);
let { scrollTop, scrollHeight } = document.body.parentNode;
if (scrollTop > scrollHeight * 0.75) {
if (posts && posts.length < maximum) {
posts.more();
}
}
} }
}, [ posts ]);
if (!posts) {
return null;
}
return (
<div className="posts">
{posts.map(renderPost)}
</div>
);
function renderPost(post, i) {
let media = _.find(medias, { id: post.featured_media });
return <PostListView route={route} post={post} media={media} key={post.id} />
} }
componentWillUnmount() { function loadMore(fraction) {
document.removeEventListener('scroll', this.handleScroll); const { scrollTop, scrollHeight } = document.body.parentNode;
} if (scrollTop > scrollHeight * fraction) {
if (posts && posts.more && posts.length < maximum) {
handleScroll = (evt) => {
let { posts, maximum } = this.props;
let { scrollTop, scrollHeight } = document.body.parentNode;
if (scrollTop > scrollHeight * 0.5) {
if (posts && posts.length < maximum) {
posts.more(); posts.more();
} }
} }
} }
} }
@@ -65,18 +54,6 @@ PostList.defaultProps = {
maximum: 500, 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 { export {
PostList as default, PostList as default,
PostList, PostList,

View File

@@ -1,52 +1,17 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React, { useState } from 'react';
import HTML from 'widgets/html'; import HTML from 'widgets/html';
import ImageDialog from 'widgets/image-dialog'; import ImageDialog from 'widgets/image-dialog';
class PostView extends PureComponent { function PostView(props) {
static displayName = 'PostView'; const { post, author, transform } = props;
const [ imageURL, setImageURL ] = useState(null);
constructor(props) { const handleClick = (evt) => {
super(props); const target = evt.target;
this.state = { const container = evt.currentTarget;
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 (
<div className="post">
<div className="meta">
<div className="date">{date}</div>
<div className="author">{name}</div>
</div>
<h1><HTML text={title} /></h1>
<div className="content" onClick={this.handleClick}>
<HTML text={content} transform={transform} />
</div>
{this.renderImageDialog()}
</div>
);
}
renderImageDialog() {
let { imageURL } = this.state;
return <ImageDialog imageURL={imageURL} onClose={this.handleDialogClose} />;
}
handleClick = (evt) => {
let target = evt.target;
let container = evt.currentTarget;
if (target.tagName === 'IMG') { if (target.tagName === 'IMG') {
let link; let link;
for (let p = target; p && p !== container; p = p.parentNode) { for (let p = target; p && p !== container; p = p.parentNode) {
@@ -56,26 +21,33 @@ class PostView extends PureComponent {
} }
} }
if (link) { if (link) {
let imageURL = link.href; setImageURL(link.href);
this.setState({ imageURL });
evt.preventDefault(); 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 (
<div className="post">
<div className="meta">
<div className="date">{date}</div>
<div className="author">{name}</div>
</div>
<h1><HTML text={title} /></h1>
<div className="content" onClick={handleClick}>
<HTML text={content} transform={transform} />
</div>
<ImageDialog imageURL={imageURL} onClose={handleDialogClose} />
</div>
);
} }
export { export {

View File

@@ -1,160 +1,134 @@
import _ from 'lodash'; import _ from 'lodash';
import Moment from 'moment'; import Moment from 'moment';
import React, { PureComponent } from 'react'; import React, { useState } from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
class SideNav extends AsyncComponent { async function SideNav(props) {
static displayName = 'SideNav'; 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) { const handleYearClick = (evt) => {
super(props); const year = parseInt(evt.currentTarget.getAttribute('data-year'));
let { route } = this.props; if (selectedYear !== year) {
let { date } = route.params; setSelectedYear(year);
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(<SideNavSync {...props} />);
// get all categories
props.categories = await wp.fetchCategories();
meanwhile.show(<SideNavSync {...props} />);
// get top tags
props.tags = await wp.fetchTopTags();
meanwhile.show(<SideNavSync {...props} />);
// 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(<SideNavSync {...props} />);
}
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(<SideNavSync {...props} />);
}
}
}
// 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(<SideNavSync {...props} />);
}
}
// 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(<SideNavSync {...props} />);
}
}
} catch (err) {
}
}
return <SideNavSync {...props} />;
}
handleYearSelect = (evt) => {
let { selectedYear } = this.state;
if (selectedYear !== evt.year) {
selectedYear = evt.year;
} else { } 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 { const postLists = [];
static displayName = 'SideNavSync'; 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() { // load the posts of each category
return ( 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(
<div className="side-nav"> <div className="side-nav">
{this.renderCategories()} {renderCategories()}
{this.renderTags()} {renderTags()}
{this.renderArchives()} {renderArchives()}
</div> </div>
) );
} }
renderCategories() { function renderCategories() {
let { categories } = this.props;
// only top-level categories // only top-level categories
categories = _.filter(categories, { parent: 0 }); const subcategories = _.filter(categories, { parent: 0 });
// don't show categories with no post // don't show categories with no post
categories = _.filter(categories, 'count'); const filtered = _.filter(subcategories, 'count');
categories = _.orderBy(categories, [ 'name' ], [ 'asc' ]); const ordered = _.orderBy(filtered, [ 'name' ], [ 'asc' ]);
if (_.isEmpty(categories)) { if (_.isEmpty(ordered)) {
return null; return null;
} }
return ( return (
<div> <div>
<h3>Categories</h3> <h3>Categories</h3>
<ul className="categories"> <ul className="categories">
{ {ordered.map(renderCategory)}
categories.map((category, i) => {
return this.renderCategory(category, i);
})
}
</ul> </ul>
</div> </div>
); );
} }
renderCategory(category, i) { function renderCategory(category, i) {
let { route, postLists } = this.props;
let { categorySlug } = route.params; let { categorySlug } = route.params;
let name = _.get(category, 'name', ''); let name = _.get(category, 'name', '');
let description = _.unescape(_.get(category, 'description', '').replace(/&#039;/g, "'")); let description = _.unescape(_.get(category, 'description', '').replace(/&#039;/g, "'"));
@@ -171,37 +145,31 @@ class SideNavSync extends PureComponent {
return ( return (
<li key={i}> <li key={i}>
<a className={className} href={url} title={description}>{name}</a> <a className={className} href={url} title={description}>{name}</a>
{this.renderSubcategories(category)} {renderSubcategories(category)}
</li> </li>
); );
} }
renderTags() { function renderTags() {
let { tags } = this.props;
// don't show tags with no post // don't show tags with no post
tags = _.filter(tags, 'count'); const activeTags = _.filter(tags, 'count');
// list tags with more posts first // list tags with more posts first
tags = _.orderBy(tags, [ 'count', 'name' ], [ 'desc', 'asc' ]); const ordered = _.orderBy(activeTags, [ 'count', 'name' ], [ 'desc', 'asc' ]);
if (_.isEmpty(tags)) { if (_.isEmpty(ordered)) {
return null; return null;
} }
return ( return (
<div> <div>
<h3>Tags</h3> <h3>Tags</h3>
<div className="tags"> <div className="tags">
{ {ordered.map(renderTag)}
tags.map((tag, i) => { {renderMoreTagButton()}
return this.renderTag(tag, i);
})
}
{this.renderMoreTagButton()}
</div> </div>
</div> </div>
); );
} }
renderTag(tag, i) { function renderTag(tag, i) {
let { route, postLists } = this.props;
let { tagSlug } = route.params; let { tagSlug } = route.params;
let name = _.get(tag, 'name', ''); let name = _.get(tag, 'name', '');
let description = _.unescape(_.get(tag, 'description', '').replace(/&#039;/g, "'")); let description = _.unescape(_.get(tag, 'description', '').replace(/&#039;/g, "'"));
@@ -223,38 +191,31 @@ class SideNavSync extends PureComponent {
); );
} }
renderMoreTagButton() { function renderMoreTagButton() {
let { tags } = this.props;
if (!_.some(tags, 'count')) { if (!_.some(tags, 'count')) {
return null; return null;
} }
if (!(tags.length < tags.total) || tags.length >= 100) { if (!(tags.length < tags.total) || tags.length >= 100) {
return null; return null;
} }
return <a className="more" onClick={this.handleMoreTagClick}>... more</a>; return <a className="more" onClick={handleMoreTagClick}>... more</a>;
} }
renderSubcategories(category) { function renderSubcategories(category) {
let { categories } = this.props; const subcategories = _.filter(categories, { parent: category.id });
let subcategories = _.filter(categories, { parent: category.id }); const filtered = _.filter(subcategories, 'count');
subcategories = _.filter(subcategories, 'count'); const ordered = _.orderBy(filtered, [ 'count', 'name' ], [ 'desc', 'asc' ]);
subcategories = _.orderBy(subcategories, [ 'count', 'name' ], [ 'desc', 'asc' ]); if (_.isEmpty(ordered)) {
if (_.isEmpty(subcategories)) {
return null; return null;
} }
return ( return (
<ul className="subcategories"> <ul className="subcategories">
{ {ordered.map(renderCategory)}
subcategories.map((subcategory, i) => {
return this.renderCategory(subcategory, i);
})
}
</ul> </ul>
); );
} }
renderArchives() { function renderArchives() {
let { archives } = this.props;
if (_.isEmpty(archives)) { if (_.isEmpty(archives)) {
return null; return null;
} }
@@ -262,109 +223,68 @@ class SideNavSync extends PureComponent {
<div> <div>
<h3>Archives</h3> <h3>Archives</h3>
<ul className="archives"> <ul className="archives">
{ {archives.map(renderYear)}
archives.map((yearEntry, i) => {
return this.renderYear(yearEntry, i);
})
}
</ul> </ul>
</div> </div>
); );
} }
renderYear(yearEntry, i) { function renderYear(yearEntry, i) {
let { selectedYear } = this.props; const listClassNames = [ 'months'] ;
let listClass = 'months';
if (yearEntry.year !== selectedYear) { if (yearEntry.year !== selectedYear) {
listClass += ' collapsed'; listClassNames.push('collapsed');
} }
return ( return (
<li key={i}> <li key={i}>
<a className="year" data-year={yearEntry.year} onClick={this.handleYearClick}> <a className="year" data-year={yearEntry.year} onClick={handleYearClick}>
{yearEntry.label} {yearEntry.label}
</a> </a>
<ul className={listClass}> <ul className={listClassNames.join(' ')}>
{ {yearEntry.months.map(renderMonth)}
yearEntry.months.map((entry, i) => {
return this.renderMonth(entry, i);
})
}
</ul> </ul>
</li> </li>
) )
} }
renderMonth(monthEntry, i) { function renderMonth(monthEntry, i) {
let { route, postLists, selectedYear } = this.props; const { date } = route.params;
let { date } = route.params; const classNames = [];
let className, url; let url;
if (monthEntry.year !== selectedYear) { if (monthEntry.year !== selectedYear) {
return null; return null;
} }
if (date && monthEntry.year === date.year && monthEntry.month === date.month) { 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)) { if (!postList || !_.isEmpty(postList.posts)) {
url = route.prefetchArchiveURL(monthEntry); url = route.prefetchArchiveURL(monthEntry);
} else { } else {
className = 'disabled'; classNames.push('disabled');
} }
return ( return (
<li key={i}> <li key={i}>
<a className={className} href={url}>{monthEntry.label}</a> <a className={classNames.join(' ')} href={url}>
{monthEntry.label}
</a>
</li> </li>
); );
} }
handleYearClick = (evt) => { function hasRecentPost(postList, daysOld) {
let { onYearSelect } = this.props; if (!postList || _.isEmpty(postList.posts)) {
let year = parseInt(evt.currentTarget.getAttribute('data-year')); return false;
if (onYearSelect) {
onYearSelect({
type: 'yearselect',
target: this,
year,
});
} }
} const post = _.first(postList.posts);
const limit = Moment().subtract(daysOld, 'day');
handleMoreTagClick = (evt) => { const publicationDate = Moment(post.date_gmt);
let { tags } = this.props; return limit < publicationDate;
tags.more();
} }
} }
function hasRecentPost(postList, daysOld) { const component = Relaks(SideNav);
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,
};
}
export { export {
SideNav as default, component as default,
SideNav, component as SideNav,
SideNavSync,
}; };

View File

@@ -1,30 +1,20 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
class TagList extends PureComponent { function TagList(props) {
static displayName = 'TagList'; const { route, tags } = props;
if (_.isEmpty(tags)) {
render() { return null;
let { tags } = this.props;
if (_.isEmpty(tags)) {
return null;
}
return (
<div className="tag-list">
<b>Tags: </b>
{
tags.map((tag, i) => {
return this.renderTag(tag, i);
})
}
</div>
);
} }
return (
<div className="tag-list">
<b>Tags: </b> {tags.map(renderTag)}
</div>
);
renderTag(tag, i) { function renderTag(tag, i) {
let { route } = this.props; const name = _.get(tag, 'name', '');
let name = _.get(tag, 'name', ''); const url = route.prefetchObjectURL(tag);
let url = route.prefetchObjectURL(tag);
return ( return (
<span key={i}> <span key={i}>
<a href={url}>{name}</a> <a href={url}>{name}</a>
@@ -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 { export {
TagList as default, TagList as default,
TagList, TagList,

View File

@@ -1,52 +1,46 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React from 'react';
import { AsyncComponent } from 'relaks'; import Relaks, { useProgress, useSaveBuffer } from 'relaks/hooks';
import { Route } from 'routing';
import WordPress from 'wordpress';
class TopNav extends AsyncComponent { async function TopNav(props) {
static displayName = 'TopNav'; 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) { const handleSearchChange = (evt) => {
let { wp, route } = this.props; setSearch(evt.target.value);
let props = { };
route,
};
meanwhile.show(<TopNavSync {...props} />);
props.site = await wp.fetchSite();
meanwhile.show(<TopNavSync {...props} />);
props.pages = await wp.fetchPages();
return <TopNavSync {...props} />;
}
}
class TopNavSync extends PureComponent { render();
static displayName = 'TopNavSync'; const site = await wp.fetchSite();
render();
const pages = await wp.fetchPages();
render();
constructor(props) { function render() {
super(props); show(
let { route } = props; <div className="top-nav">
let { search } = route.params; {renderTitleBar()}
this.searchTimeout = 0; {renderPageLinkBar()}
this.state = { search }; {renderSearchBar()}
}
render() {
let { onMouseOver, onMouseOut } = this.props;
return (
<div className="top-nav" onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
{this.renderTitleBar()}
{this.renderPageLinkBar()}
{this.renderSearchBar()}
</div> </div>
); );
} }
renderTitleBar() { function renderTitleBar() {
let { route, site } = this.props; const name = _.get(site, 'name', '');
let name = _.get(site, 'name', ''); const descriptionHTML = _.get(site, 'description', '');
let description = _.unescape(_.get(site, 'description', '').replace(/&#039;/g, "'")); const description = _.unescape(descriptionHTML.replace(/&#039;/g, "'"));
let url = route.getRootURL(); const url = route.getRootURL();
return ( return (
<div className="title-bar"> <div className="title-bar">
<div className="title" title={description}> <div className="title" title={description}>
@@ -59,25 +53,19 @@ class TopNavSync extends PureComponent {
); );
} }
renderPageLinkBar() { function renderPageLinkBar() {
let { pages } = this.props; let filtered = _.filter(pages, { parent: 0 });
pages = _.filter(pages, { parent: 0 }); let ordered = _.sortBy(filtered, 'menu_order');
pages = _.sortBy(pages, 'menu_order');
return ( return (
<div className="page-bar"> <div className="page-bar">
{ {ordered.map(renderPageLinkButton)}
pages.map((page, i) => {
return this.renderPageLinkButton(page, i);
})
}
</div> </div>
); );
} }
renderPageLinkButton(page, i) { function renderPageLinkButton(page, i) {
let { route } = this.props; const title = _.get(page, 'title.rendered');
let title = _.get(page, 'title.rendered'); const url = route.prefetchObjectURL(page);
let url = route.prefetchObjectURL(page);
return ( return (
<div className="button" key={i}> <div className="button" key={i}>
<a href={url}>{title}</a> <a href={url}>{title}</a>
@@ -85,71 +73,21 @@ class TopNavSync extends PureComponent {
); );
} }
renderSearchBar() { function renderSearchBar() {
let { route } = this.props;
let { search } = this.state;
return ( return (
<div className="search-bar"> <div className="search-bar">
<span className="input-container"> <span className="input-container">
<input type="text" value={search || ''} onChange={this.handleSearchChange} /> <input type="text" value={search || ''} onChange={handleSearchChange} />
<i className="fa fa-search" /> <i className="fa fa-search" />
</span> </span>
</div> </div>
); );
} }
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 = { const component = Relaks(TopNav);
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,
};
}
export { export {
TopNav as default, component as default,
TopNav, component as TopNav,
TopNavSync,
}; };

View File

@@ -14,7 +14,8 @@ module.exports = function(config) {
}) })
} }
} }
}) });
config.resolve.modules.push(Path.resolve('./node_modules'));
}; };
function resolve(type, module) { function resolve(type, module) {