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:
@@ -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');
|
||||
}
|
||||
|
38
src/main.js
38
src/main.js
@@ -6,50 +6,50 @@ import { Route, routes } from 'routing';
|
||||
import WordpressDataSource from 'relaks-wordpress-data-source';
|
||||
import RouteManager from 'relaks-route-manager';
|
||||
import { harvest } from 'relaks-harvest';
|
||||
import Relaks, { plant } from 'relaks';
|
||||
import { plant } from 'relaks/hooks';
|
||||
|
||||
if (process.env.TARGET === 'browser') {
|
||||
async function initialize(evt) {
|
||||
// create data source
|
||||
let host = process.env.DATA_HOST || `${location.protocol}//${location.host}`;
|
||||
let basePath = process.env.BASE_PATH;
|
||||
let dataSource = new WordpressDataSource({
|
||||
const host = process.env.DATA_HOST || `${location.protocol}//${location.host}`;
|
||||
const basePath = process.env.BASE_PATH;
|
||||
const dataSource = new WordpressDataSource({
|
||||
baseURL: host + basePath + 'json',
|
||||
});
|
||||
dataSource.activate();
|
||||
|
||||
// create route manager
|
||||
let routeManager = new RouteManager({
|
||||
const routeManager = new RouteManager({
|
||||
routes,
|
||||
basePath,
|
||||
useHashFallback: (location.protocol !== 'http:' && location.protocol !== 'https:'),
|
||||
});
|
||||
routeManager.addEventListener('beforechange', (evt) => {
|
||||
let route = new Route(routeManager, dataSource);
|
||||
const route = new Route(routeManager, dataSource);
|
||||
evt.postponeDefault(route.setParameters(evt, true));
|
||||
});
|
||||
routeManager.activate();
|
||||
await routeManager.start();
|
||||
|
||||
let container = document.getElementById('react-container');
|
||||
const container = document.getElementById('react-container');
|
||||
if (!process.env.DATA_HOST) {
|
||||
// there is SSR support when we're fetching data from the same host
|
||||
// as the HTML page
|
||||
let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
|
||||
let seeds = await harvest(ssrElement, { seeds: true });
|
||||
const ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
|
||||
const seeds = await harvest(ssrElement, { seeds: true });
|
||||
plant(seeds);
|
||||
hydrate(ssrElement, container);
|
||||
}
|
||||
let csrElement = createElement(FrontEnd, { dataSource, routeManager });
|
||||
const csrElement = createElement(FrontEnd, { dataSource, routeManager });
|
||||
render(csrElement, container);
|
||||
|
||||
// check for changes periodically
|
||||
let mtimeURL = host + basePath + '.mtime';
|
||||
const mtimeURL = host + basePath + '.mtime';
|
||||
let mtimeLast;
|
||||
for (;;) {
|
||||
try {
|
||||
let res = await fetch(mtimeURL);
|
||||
let mtime = await res.text();
|
||||
const res = await fetch(mtimeURL);
|
||||
const mtime = await res.text();
|
||||
if (mtime !== mtimeLast) {
|
||||
if (mtimeLast) {
|
||||
dataSource.invalidate();
|
||||
@@ -65,28 +65,28 @@ if (process.env.TARGET === 'browser') {
|
||||
window.addEventListener('load', initialize);
|
||||
} else if (process.env.TARGET === 'node') {
|
||||
async function serverSideRender(options) {
|
||||
let basePath = process.env.BASE_PATH;
|
||||
let dataSource = new WordpressDataSource({
|
||||
const basePath = process.env.BASE_PATH;
|
||||
const dataSource = new WordpressDataSource({
|
||||
baseURL: options.host + basePath + 'json',
|
||||
fetchFunc: options.fetch,
|
||||
});
|
||||
dataSource.activate();
|
||||
|
||||
let routeManager = new RouteManager({
|
||||
const routeManager = new RouteManager({
|
||||
routes,
|
||||
basePath,
|
||||
});
|
||||
routeManager.addEventListener('beforechange', (evt) => {
|
||||
let route = new Route(routeManager, dataSource);
|
||||
const route = new Route(routeManager, dataSource);
|
||||
evt.postponeDefault(route.setParameters(evt, false));
|
||||
});
|
||||
routeManager.activate();
|
||||
await routeManager.start(options.path);
|
||||
|
||||
let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: options.target });
|
||||
const ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: options.target });
|
||||
return harvest(ssrElement);
|
||||
}
|
||||
|
||||
exports.render = serverSideRender;
|
||||
exports.basePath = process.env.BASE_PATH;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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
124
src/props.js
Normal 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,
|
||||
};
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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, '<$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, '<$1<');
|
||||
return ReactHtmlParser(fixed, options);
|
||||
}
|
||||
|
||||
export {
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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(/'/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(/'/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,
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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(/'/g, "'"));
|
||||
let url = route.getRootURL();
|
||||
function renderTitleBar() {
|
||||
const name = _.get(site, 'name', '');
|
||||
const descriptionHTML = _.get(site, 'description', '');
|
||||
const description = _.unescape(descriptionHTML.replace(/'/g, "'"));
|
||||
const url = route.getRootURL();
|
||||
return (
|
||||
<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,
|
||||
};
|
||||
|
@@ -14,7 +14,8 @@ module.exports = function(config) {
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
config.resolve.modules.push(Path.resolve('./node_modules'));
|
||||
};
|
||||
|
||||
function resolve(type, module) {
|
||||
|
Reference in New Issue
Block a user