1
0
mirror of https://github.com/trambarhq/relaks-wordpress-example.git synced 2025-09-02 12:42:38 +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 { 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 (
<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 });
});
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 (
<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) => {
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');
}

View File

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

View File

@@ -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(<ArchivePageSync {...props} />);
props.posts = await wp.fetchPostsInMonth(date);
return <ArchivePageSync {...props} />;
}
}
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(
<div className="page">
<Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={100} />
@@ -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,
};

View File

@@ -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(<CategoryPageSync {...props} />);
props.category = await wp.fetchCategory(categorySlug);
props.parentCategories = await wp.fetchParentCategories(props.category);
meanwhile.show(<CategoryPageSync {...props} />);
props.posts = await wp.fetchPostsInCategory(props.category);
return <CategoryPageSync {...props} />;
}
}
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(
<div className="page">
<Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} />
@@ -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,
};

View File

@@ -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(<PagePageSync {...props} />);
props.page = await wp.fetchPage(pageSlug);
props.parentPages = await wp.fetchParentPages(props.page);
meanwhile.show(<PagePageSync {...props} />);
props.childPages = await wp.fetchChildPages(props.page);
return <PagePageSync {...props} />;
}
}
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: <HTML text={title} />, url })
}
}
return (
show(
<div className="page">
<Breadcrumb trail={trail} />
<PageView page={page} transform={route.transformNode} />
@@ -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,
};

View File

@@ -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(<PostPageSync {...props} />);
props.post = await wp.fetchPost(postSlug);
meanwhile.show(<PostPageSync {...props} />);
props.categories = await this.findCategoryChain(props.post);
meanwhile.show(<PostPageSync {...props} />);
props.author = await wp.fetchAuthor(props.post);
meanwhile.show(<PostPageSync {...props} />);
props.tags = await wp.fetchTagsOfPost(props.post);
if (!wp.ssr) {
meanwhile.show(<PostPageSync {...props} />);
props.comments = await wp.fetchComments(props.post);
}
return <PostPageSync {...props} />;
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(
<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 [];
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 (
<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),
};
}
const component = Relaks(PostPage);
export {
PostPage as default,
PostPage,
PostPageSync,
component as default,
component as PostPage,
};

View File

@@ -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(<SearchPageSync {...props} />);
props.posts = await wp.fetchMatchingPosts(search);
return <SearchPageSync {...props} />;
}
}
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(
<div className="page">
<Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} maximum={1000} />
@@ -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,
};

View File

@@ -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(<TagPageSync {...props} />);
props.tag = await wp.fetchTag(tagSlug);
meanwhile.show(<TagPageSync {...props} />);
props.posts = await wp.fetchPostsWithTag(props.tag);
return <TagPageSync {...props} />;
}
}
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(
<div className="page">
<Breadcrumb trail={trail} />
<PostList route={route} posts={posts} minimum={40} />
@@ -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,
};

View File

@@ -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(<WelcomePageSync {...props} />)
props.posts = await wp.fetchPosts();
meanwhile.show(<WelcomePageSync {...props} />)
props.medias = await wp.fetchFeaturedMedias(props.posts, 10);
return <WelcomePageSync {...props} />;
}
}
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(
<div className="page">
<PostList route={route} posts={posts} medias={medias} minimum={40} />
</div>
@@ -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,
};

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 {
static displayName = 'Breadcrumb';
render() {
let { trail } = this.props;
let children = []
let key = 0;
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>;
function Breadcrumb(props) {
const { trail } = props;
const children = []
let key = 0;
for (let item of trail) {
children.push(<a key={key++} href={item.url}>{item.label}</a>);
children.push(' > ');
}
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
Breadcrumb.propTypes = {
trail: PropTypes.arrayOf(PropTypes.object),
};
children.pop();
return <h4 className="breadcrumb">{children}</h4>;
}
export {

View File

@@ -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 (
<div className="comment-list-view">
<div className="commenter">
<img className="avatar" src={avatarURL} />
<span className="name">{name}:</span>
</div>
<HTML text={content} />
{this.renderReplies()}
return (
<div className="comment-list-view">
<div className="commenter">
<img className="avatar" src={avatarURL} />
<span className="name">{name}:</span>
</div>
);
}
<HTML text={content} />
{renderReplies()}
</div>
);
renderReplies() {
let { comment, allComments } = this.props;
function renderReplies() {
if (!_.some(allComments, { parent: comment.id })) {
return null;
}
@@ -33,19 +29,10 @@ class CommentListView extends PureComponent {
<div className="replies">
<CommentList allComments={allComments} parentCommentID={comment.id} />
</div>
)
);
}
}
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,

View File

@@ -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 (
<div className="comments">
{
_.map(comments, (comment) => {
return <CommentListView comment={comment} allComments={allComments} key={comment.id} />;
})
}
</div>
)
return (
<div className="comments">
{comments.map(renderComment)}
</div>
);
function renderComment(comment, i) {
return <CommentListView comment={comment} allComments={allComments} key={comment.id} />;
}
}
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,

View File

@@ -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 (
<div className="comment-section">
<h3>Comments</h3>
<CommentList allComments={comments} parentCommentID={0} />
</div>
);
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 (
<div className="comment-section">
<h3>Comments</h3>
<CommentList allComments={comments} parentCommentID={0} />
</div>
);
}
export {

View File

@@ -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(/<([^>]*)</g, '&lt;$1<');
return ReactHtmlParser(text, options);
}
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
HTML.propTypes = {
text: PropTypes.string,
transform: PropTypes.func,
};
function HTML(props) {
const { text, transform } = props;
const options = { transform };
// fix unescaped <
const fixed = text.replace(/<([^>]*)</g, '&lt;$1<');
return ReactHtmlParser(fixed, options);
}
export {

View File

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

View File

@@ -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 <img {...props} />;
function MediaView(props) {
const { media, size } = props;
let info = _.get(media, [ 'media_details', 'sizes', size ]);
if (!info) {
info = media;
}
return <img src={info.source_url} width={info.width} height={info.height} />;
}
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,

View File

@@ -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 (
<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,
};
function PageListView(props) {
const { route, page } = props;
const title = _.get(page, 'title.rendered', '');
const url = route.prefetchObjectURL(page);
return (
<div className="page-list-view">
<a href={url}><HTML text={title} /></a>
</div>
);
}
export {

View File

@@ -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 (
<ul className="pages">
{pages.map(renderPage)}
</ul>
);
function renderPage(page, i) {
return (
<ul className="pages">
{
pages.map((page) => {
return (
<li key={page.id}>
<PageListView route={route} page={page} />
</li>
);
})
}
</ul>
)
<li key={page.id}>
<PageListView route={route} page={page} />
</li>
);
}
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 {

View File

@@ -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 (
<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>
return (
<div className="page">
<div className="meta">
<div className="date">{date}</div>
</div>
);
}
}
if (process.env.NODE_ENV !== 'production') {
const PropTypes = require('prop-types');
PageView.propTypes = {
page: PropTypes.object,
transform: PropTypes.func,
};
<h1><HTML text={title} /></h1>
<div className="content">
<HTML text={content} transform={transform}/>
</div>
</div>
);
}
export {

View File

@@ -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 (
<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>
if (media) {
return (
<div className="post-list-view with-media">
<div className="media">
<MediaView media={media} />
</div>
);
} else {
return (
<div className="post-list-view">
<div className="text">
<div className="headline">
<h3 className="title">
<a href={url}><HTML text={title} /></a>
@@ -51,28 +31,33 @@ class PostListView extends PureComponent {
<HTML text={excerpt} />
</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 {
PostListView as default,
PostListView,

View File

@@ -1,62 +1,51 @@
import _ from 'lodash';
import Moment from 'moment';
import React, { PureComponent } from 'react';
import { Route } from 'routing';
import React, { useEffect } from 'react';
import PostListView from 'widgets/post-list-view';
class PostList extends PureComponent {
static displayName = 'PostList'
function PostList(props) {
const { route, posts, medias, minimum, maximum } = props;
render() {
let { route, posts, medias } = this.props;
if (!posts) {
return null;
}
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>
);
}
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 (
<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() {
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,

View File

@@ -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 (
<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;
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 (
<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 {

View File

@@ -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(<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;
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(
<div className="side-nav">
{this.renderCategories()}
{this.renderTags()}
{this.renderArchives()}
{renderCategories()}
{renderTags()}
{renderArchives()}
</div>
)
);
}
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 (
<div>
<h3>Categories</h3>
<ul className="categories">
{
categories.map((category, i) => {
return this.renderCategory(category, i);
})
}
{ordered.map(renderCategory)}
</ul>
</div>
);
}
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(/&#039;/g, "'"));
@@ -171,37 +145,31 @@ class SideNavSync extends PureComponent {
return (
<li key={i}>
<a className={className} href={url} title={description}>{name}</a>
{this.renderSubcategories(category)}
{renderSubcategories(category)}
</li>
);
}
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 (
<div>
<h3>Tags</h3>
<div className="tags">
{
tags.map((tag, i) => {
return this.renderTag(tag, i);
})
}
{this.renderMoreTagButton()}
{ordered.map(renderTag)}
{renderMoreTagButton()}
</div>
</div>
);
}
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(/&#039;/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 <a className="more" onClick={this.handleMoreTagClick}>... more</a>;
return <a className="more" onClick={handleMoreTagClick}>... more</a>;
}
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 (
<ul className="subcategories">
{
subcategories.map((subcategory, i) => {
return this.renderCategory(subcategory, i);
})
}
{ordered.map(renderCategory)}
</ul>
);
}
renderArchives() {
let { archives } = this.props;
function renderArchives() {
if (_.isEmpty(archives)) {
return null;
}
@@ -262,109 +223,68 @@ class SideNavSync extends PureComponent {
<div>
<h3>Archives</h3>
<ul className="archives">
{
archives.map((yearEntry, i) => {
return this.renderYear(yearEntry, i);
})
}
{archives.map(renderYear)}
</ul>
</div>
);
}
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 (
<li key={i}>
<a className="year" data-year={yearEntry.year} onClick={this.handleYearClick}>
<a className="year" data-year={yearEntry.year} onClick={handleYearClick}>
{yearEntry.label}
</a>
<ul className={listClass}>
{
yearEntry.months.map((entry, i) => {
return this.renderMonth(entry, i);
})
}
<ul className={listClassNames.join(' ')}>
{yearEntry.months.map(renderMonth)}
</ul>
</li>
)
}
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 (
<li key={i}>
<a className={className} href={url}>{monthEntry.label}</a>
<a className={classNames.join(' ')} href={url}>
{monthEntry.label}
</a>
</li>
);
}
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,
};

View File

@@ -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 (
<div className="tag-list">
<b>Tags: </b>
{
tags.map((tag, i) => {
return this.renderTag(tag, i);
})
}
</div>
);
function TagList(props) {
const { route, tags } = props;
if (_.isEmpty(tags)) {
return null;
}
return (
<div className="tag-list">
<b>Tags: </b> {tags.map(renderTag)}
</div>
);
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 (
<span key={i}>
<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 {
TagList as default,
TagList,

View File

@@ -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(<TopNavSync {...props} />);
props.site = await wp.fetchSite();
meanwhile.show(<TopNavSync {...props} />);
props.pages = await wp.fetchPages();
return <TopNavSync {...props} />;
}
}
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 (
<div className="top-nav" onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
{this.renderTitleBar()}
{this.renderPageLinkBar()}
{this.renderSearchBar()}
function render() {
show(
<div className="top-nav">
{renderTitleBar()}
{renderPageLinkBar()}
{renderSearchBar()}
</div>
);
}
renderTitleBar() {
let { route, site } = this.props;
let name = _.get(site, 'name', '');
let description = _.unescape(_.get(site, 'description', '').replace(/&#039;/g, "'"));
let url = route.getRootURL();
function renderTitleBar() {
const name = _.get(site, 'name', '');
const descriptionHTML = _.get(site, 'description', '');
const description = _.unescape(descriptionHTML.replace(/&#039;/g, "'"));
const url = route.getRootURL();
return (
<div className="title-bar">
<div className="title" title={description}>
@@ -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 (
<div className="page-bar">
{
pages.map((page, i) => {
return this.renderPageLinkButton(page, i);
})
}
{ordered.map(renderPageLinkButton)}
</div>
);
}
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 (
<div className="button" key={i}>
<a href={url}>{title}</a>
@@ -85,71 +73,21 @@ class TopNavSync extends PureComponent {
);
}
renderSearchBar() {
let { route } = this.props;
let { search } = this.state;
function renderSearchBar() {
return (
<div className="search-bar">
<span className="input-container">
<input type="text" value={search || ''} onChange={this.handleSearchChange} />
<input type="text" value={search || ''} onChange={handleSearchChange} />
<i className="fa fa-search" />
</span>
</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 = {
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,
};

View File

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