diff --git a/package-lock.json b/package-lock.json
index 0c0708a..02db5ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4879,9 +4879,9 @@
}
},
"lodash": {
- "version": "4.17.10",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
- "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"lodash._baseclone": {
@@ -5247,6 +5247,12 @@
"minimist": "0.0.8"
}
},
+ "moment": {
+ "version": "2.23.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
+ "integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==",
+ "dev": true
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
diff --git a/package.json b/package.json
index 60cecf5..91bb44e 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,10 @@
"extract-text-webpack-plugin": "^3.0.2",
"hammerjs": "^2.0.8",
"html-webpack-plugin": "^2.28.0",
+ "lodash": "^4.17.11",
+ "moment": "^2.23.0",
"node-sass": "^4.5.3",
+ "prop-types": "^15.6.2",
"react": "^16.6.3",
"regenerator-runtime": "^0.12.0",
"relaks": "^1.1.7",
diff --git a/src/main.js b/src/main.js
index 39162d5..ba2ebf9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -13,6 +13,10 @@ if (typeof(window) === 'object') {
async function initialize(evt) {
// create data source
let host = `${location.protocol}//${location.host}`;
+ if (process.env.NODE_ENV !== 'production' && process.env.WEBPACK_DEV_SERVER) {
+ // use hardcoded URL when we're running in dev-server
+ host = 'http://localhost:8000';
+ }
let dataSource = new WordpressDataSource({
baseURL: `${host}/wp-json`,
});
@@ -31,10 +35,13 @@ if (typeof(window) === 'object') {
if (!appContainer) {
throw new Error('Unable to find app element in DOM');
}
- //let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
- //let seeds = await harvest(ssrElement, { seeds: true });
- //Relaks.set('seeds', seeds);
- //hydrate(ssrElement, appContainer);
+ // expect SSR unless we're running in dev-server
+ if (!(process.env.NODE_ENV !== 'production' && process.env.WEBPACK_DEV_SERVER)) {
+ let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });
+ let seeds = await harvest(ssrElement, { seeds: true });
+ Relaks.set('seeds', seeds);
+ hydrate(ssrElement, appContainer);
+ }
let appElement = createElement(FrontEnd, { dataSource, routeManager });
render(appElement, appContainer);
diff --git a/src/style.scss b/src/style.scss
index d46f609..9169f20 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -18,12 +18,6 @@ BODY {
A {
&:link, &:visited {
text-decoration: none;
- color: #000;
- }
-
- &:hover {
- text-decoration: underline;
- color: #f11;
}
}
@@ -35,6 +29,30 @@ A {
bottom: 0;
background-color: #66023c;
overflow: hidden;
+ color: #cccccc;
+
+ A {
+ color: #cccccc;
+ }
+
+ .archive {
+ LI {
+ margin-top: 0.1em;
+ margin-bottom: 0.1em;
+ }
+
+ .year {
+ cursor: pointer;
+ }
+
+ .months {
+ overflow: hidden;
+
+ &.collapsed {
+ height: 0;
+ }
+ }
+ }
}
.top-nav {
@@ -44,6 +62,10 @@ A {
width: calc(100% - 18em);
background-color: #990000; // prevent visual glitch in Android
+ A {
+ color: #cccccc;
+ }
+
.title-bar {
display: flex;
align-items: center;
diff --git a/src/widgets/side-nav.jsx b/src/widgets/side-nav.jsx
index 32a776d..50fc90c 100644
--- a/src/widgets/side-nav.jsx
+++ b/src/widgets/side-nav.jsx
@@ -1,5 +1,105 @@
+import _ from 'lodash';
+import Moment from 'moment';
import React, { PureComponent } from 'react';
import { AsyncComponent } from 'relaks';
+import { Route } from 'routing';
+import WordPress from 'wordpress';
+
+class SideNav extends AsyncComponent {
+ static displayName = 'SideNav';
+
+ constructor(props) {
+ super(props);
+ let { route } = this.props;
+ let selectedYear;
+ if (route.params.month) {
+ selectedYear = parseInt(route.params.month.substr(0, 4));
+ } else {
+ selectedYear = 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();
+ props.categories = await wp.fetchList('/wp/v2/categories/');
+ meanwhile.show();
+
+ // get the latest post and the earliest post
+ let latestPosts = await wp.fetchList('/wp/v2/posts/');
+ let latestPost = _.first(latestPosts);
+ let earliestPosts = await wp.fetchList(`/wp/v2/posts/?order=asc&per_page=1`)
+ let earliestPost = _.first(earliestPosts);
+
+ // build the archive tree
+ props.archive = [];
+ if (latestPost && earliestPost) {
+ let lastMonthEnd = Moment(latestPost.date_gmt).endOf('month');
+ let firstMonthStart = Moment(earliestPost.date_gmt).startOf('month');
+ let currentYearEntry;
+ // loop from the last month to the first
+ let e = lastMonthEnd.clone();
+ let s = e.clone().startOf('month');
+ while (s >= firstMonthStart) {
+ let year = s.year();
+ let month = s.month() + 1;
+ if (!currentYearEntry || currentYearEntry.year !== year) {
+ // start a new year
+ currentYearEntry = {
+ year,
+ label: s.format('YYYY'),
+ months: []
+ };
+ props.archive.push(currentYearEntry);
+ }
+ let monthEntry = {
+ month,
+ label: s.format('MMMM'),
+ slug: s.format('YYYY-MM'),
+ post: undefined,
+ start: s.clone(),
+ end: e.clone(),
+ };
+ currentYearEntry.months.push(monthEntry);
+
+ e.subtract(1, 'month');
+ s.subtract(1, 'month');
+ }
+ meanwhile.show();
+
+ // load the posts of the selected year
+ for (let yearEntry of props.archive) {
+ if (yearEntry.year === selectedYear) {
+ for (let monthEntry of yearEntry.months) {
+ let before = monthEntry.end.toISOString();
+ let after = monthEntry.start.toISOString();
+ let url = `/wp/v2/posts/?before=${before}&after=${after}`;
+ monthEntry.posts = await wp.fetchList(url);
+
+ // force prop change
+ props.archive = _.clone(props.archive);
+ meanwhile.show();
+ }
+ }
+ }
+ }
+ return ;
+ }
+
+ handleYearSelect = (evt) => {
+ this.setState({ selectedYear: evt.year });
+ }
+}
class SideNavSync extends PureComponent {
static displayName = 'SideNavSync';
@@ -7,13 +107,128 @@ class SideNavSync extends PureComponent {
render() {
return (
- Side-Nav
+ {this.renderCategories()}
+ {this.renderArchive()}
)
}
+
+ renderCategories() {
+ let { categories } = this.props;
+ if (!categories) {
+ return null;
+ }
+ // don't show categories with no post
+ categories = _.filter(categories, 'count');
+ // list category with more posts first
+ categories = _.orderBy(categories, [ 'count', 'name' ], [ 'desc', 'asc' ]);
+ return (
+
+ {
+ categories.map((category, i) => {
+ return this.renderCategory(category, i);
+ })
+ }
+
+ )
+ }
+
+ renderCategory(category, i) {
+ let name = _.get(category, 'name', '');
+ let description = _.get(category, 'description', '');
+ let slug = _.get(category, 'slug', '');
+ let url = '';
+ return (
+
+ {name}
+
+ );
+ }
+
+ renderArchive() {
+ let { archive } = this.props;
+ if (!archive) {
+ return null;
+ }
+ return (
+
+ {
+ archive.map((entry, i) => {
+ return this.renderYear(entry, i);
+ })
+ }
+
+ );
+ }
+
+ renderYear(yearEntry, i) {
+ let { selectedYear } = this.props;
+ let labelClass = 'year';
+ let listClass = 'months';
+ if (yearEntry.year === selectedYear) {
+ labelClass += ' selected';
+ } else {
+ listClass += ' collapsed';
+ }
+ return (
+
+
+ {yearEntry.label}
+
+
+ {
+ yearEntry.months.map((entry, i) => {
+ return this.renderMonth(entry, i);
+ })
+ }
+
+
+ )
+ }
+
+ renderMonth(monthEntry, i) {
+ let url;
+ if (!_.isEmpty(monthEntry.posts) || _.isUndefined(monthEntry.posts)) {
+ url = '#';
+ }
+ return (
+
+ {monthEntry.label}
+
+ );
+ }
+
+ handleYearClick = (evt) => {
+ let { onYearSelect } = this.props;
+ let year = parseInt(evt.currentTarget.getAttribute('data-year'));
+ if (onYearSelect) {
+ onYearSelect({
+ type: 'yearselect',
+ target: this,
+ year,
+ });
+ }
+ }
+}
+
+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),
+ archive: PropTypes.arrayOf(PropTypes.object),
+ selectedYear: PropTypes.number,
+ route: PropTypes.instanceOf(Route),
+ onYearSelect: PropTypes.func,
+ };
}
export {
- SideNavSync as default,
+ SideNav as default,
+ SideNav,
SideNavSync,
};
diff --git a/src/widgets/top-nav.jsx b/src/widgets/top-nav.jsx
index 4fc6324..48945cd 100644
--- a/src/widgets/top-nav.jsx
+++ b/src/widgets/top-nav.jsx
@@ -1,12 +1,32 @@
+import _ from 'lodash';
import React, { PureComponent } from 'react';
import { AsyncComponent } from 'relaks';
+import { Route } from 'routing';
+import WordPress from 'wordpress';
-class TopBarSync extends PureComponent {
+class TopNav extends AsyncComponent {
+ static displayName = 'TopNav';
+
+ async renderAsync(meanwhile) {
+ let { wp, route } = this.props;
+ let props = {
+ route,
+ };
+ meanwhile.show();
+ props.system = await wp.fetchOne('/');
+ meanwhile.show();
+ props.pages = await wp.fetchList('/wp/v2/pages/');
+ return ;
+ }
+}
+
+class TopNavSync extends PureComponent {
static displayName = 'TopNavSync';
render() {
+ let { onMouseOver, onMouseOut } = this.props;
return (
-
+
{this.renderTitleBar()}
{this.renderPageLinkBar()}
{this.renderSearchBar()}
@@ -15,34 +35,52 @@ class TopBarSync extends PureComponent {
}
renderTitleBar() {
+ let { system } = this.props;
+ let name = _.get(system, 'name', '');
+ let description = _.get(system, 'description', '');
return (
-
+
- Romanes eunt domus
+ {name}
);
}
renderPageLinkBar() {
+ let { pages } = this.props;
+ pages = _.sortBy(pages, 'menu_order');
return (
-
- Hello
-
-
- World
-
+ {
+ pages.map((page, i) => {
+ return this.renderPageLinkButton(page, i);
+ })
+ }
+
+ );
+ }
+
+ renderPageLinkButton(page, i) {
+ let { route } = this.props;
+ let title = _.get(page, 'title.rendered');
+ let slug = _.get(page, 'slug');
+ let url = '';
+ return (
+
);
}
renderSearchBar() {
+ let { route } = this.props;
+ let search = _.get(route.params, 'search', '');
return (
-
+
@@ -50,7 +88,28 @@ class TopBarSync extends PureComponent {
}
}
-export {
- TopBarSync as default,
- TopBarSync,
+TopNavSync.defaultProps = {
+ system: {},
+ 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 = {
+ system: PropTypes.object,
+ pages: PropTypes.arrayOf(PropTypes.object),
+ route: PropTypes.instanceOf(Route).isRequired,
+ };
+}
+
+export {
+ TopNav as default,
+ TopNav,
+ TopNavSync,
};
diff --git a/webpack.config.js b/webpack.config.js
index d3e2995..45a24e9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -57,26 +57,9 @@ var clientConfig = {
analyzerMode: (event === 'build') ? 'static' : 'disabled',
reportFilename: `report.html`,
}),
- new HtmlWebpackPlugin({
- template: Path.resolve(`./src/index.html`),
- filename: Path.resolve(`./server/www/index.html`),
- }),
new ExtractTextPlugin("styles.css"),
],
devtool: (event === 'build') ? false : 'inline-source-map',
- devServer: {
- inline: true,
- historyApiFallback: {
- rewrites: [
- {
- from: /.*/,
- to: function(context) {
- return context.parsedUrl.pathname.replace(/.*\/(.*)$/, '/$1');
- }
- }
- ]
- }
- }
};
var serverConfig = {
@@ -102,7 +85,39 @@ var serverConfig = {
devtool: clientConfig.devtool,
};
-var configs = module.exports = clientConfig; //[ clientConfig, serverConfig ];
+var configs = module.exports = [ clientConfig, serverConfig ];
+
+var isDevServer = process.argv.find((arg) => {
+ return arg.includes('webpack-dev-server') ;
+});
+if (isDevServer) {
+ // remove server config
+ configs.pop();
+ // need HTML page
+ clientConfig.plugins.push(new HtmlWebpackPlugin({
+ template: Path.resolve(`./src/index.html`),
+ filename: Path.resolve(`./server/www/index.html`),
+ }));
+ // set constant
+ var constants = {
+ 'process.env.WEBPACK_DEV_SERVER': 'true',
+ };
+ clientConfig.plugins.unshift(new DefinePlugin(constants));
+ // config dev-server to support client-side routing
+ clientConfig.devServer = {
+ inline: true,
+ historyApiFallback: {
+ rewrites: [
+ {
+ from: /.*/,
+ to: function(context) {
+ return context.parsedUrl.pathname.replace(/.*\/(.*)$/, '/$1');
+ }
+ }
+ ]
+ }
+ };
+}
var constants = {};
if (event === 'build') {
@@ -110,14 +125,13 @@ if (event === 'build') {
configs.forEach((config) => {
// set NODE_ENV to production
- var plugins = config.plugins;
var constants = {
'process.env.NODE_ENV': '"production"',
};
- plugins.unshift(new DefinePlugin(constants));
+ config.plugins.unshift(new DefinePlugin(constants));
// use Uglify to remove dead-code
- plugins.unshift(new UglifyJSPlugin({
+ config.plugins.unshift(new UglifyJSPlugin({
uglifyOptions: {
compress: {
drop_console: true,