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

Loading live data into nav.

This commit is contained in:
Chung Leong
2018-12-22 23:44:17 +01:00
parent 8cd02adfaa
commit 47a8742623
7 changed files with 376 additions and 50 deletions

12
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

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

View File

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

View File

@@ -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(<SideNavSync {...props} />);
props.categories = await wp.fetchList('/wp/v2/categories/');
meanwhile.show(<SideNavSync {...props} />);
// 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(<SideNavSync {...props} />);
// 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(<SideNavSync {...props} />);
}
}
}
}
return <SideNavSync {...props} />;
}
handleYearSelect = (evt) => {
this.setState({ selectedYear: evt.year });
}
}
class SideNavSync extends PureComponent {
static displayName = 'SideNavSync';
@@ -7,13 +107,128 @@ class SideNavSync extends PureComponent {
render() {
return (
<div className="side-nav">
Side-Nav
{this.renderCategories()}
{this.renderArchive()}
</div>
)
}
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 (
<ul className="categories">
{
categories.map((category, i) => {
return this.renderCategory(category, i);
})
}
</ul>
)
}
renderCategory(category, i) {
let name = _.get(category, 'name', '');
let description = _.get(category, 'description', '');
let slug = _.get(category, 'slug', '');
let url = '';
return (
<li key={i}>
<a href={url} title={description}>{name}</a>
</li>
);
}
renderArchive() {
let { archive } = this.props;
if (!archive) {
return null;
}
return (
<ul className="archive">
{
archive.map((entry, i) => {
return this.renderYear(entry, i);
})
}
</ul>
);
}
renderYear(yearEntry, i) {
let { selectedYear } = this.props;
let labelClass = 'year';
let listClass = 'months';
if (yearEntry.year === selectedYear) {
labelClass += ' selected';
} else {
listClass += ' collapsed';
}
return (
<li key={i}>
<span className={labelClass} data-year={yearEntry.year} onClick={this.handleYearClick}>
{yearEntry.label}
</span>
<ul className={listClass}>
{
yearEntry.months.map((entry, i) => {
return this.renderMonth(entry, i);
})
}
</ul>
</li>
)
}
renderMonth(monthEntry, i) {
let url;
if (!_.isEmpty(monthEntry.posts) || _.isUndefined(monthEntry.posts)) {
url = '#';
}
return (
<li key={i}>
<a 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,
});
}
}
}
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,
};

View File

@@ -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(<TopNavSync {...props} />);
props.system = await wp.fetchOne('/');
meanwhile.show(<TopNavSync {...props} />);
props.pages = await wp.fetchList('/wp/v2/pages/');
return <TopNavSync {...props} />;
}
}
class TopNavSync extends PureComponent {
static displayName = 'TopNavSync';
render() {
let { onMouseOver, onMouseOut } = this.props;
return (
<div className="top-nav">
<div className="top-nav" onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
{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 (
<div className="title-bar">
<div className="title">
<div className="title" title={description}>
<i className="fa fa-home" />
<span className="site-name">Romanes eunt domus</span>
<span className="site-name">{name}</span>
</div>
</div>
);
}
renderPageLinkBar() {
let { pages } = this.props;
pages = _.sortBy(pages, 'menu_order');
return (
<div className="page-bar">
<div className="button">
Hello
</div>
<div className="button">
World
</div>
{
pages.map((page, i) => {
return this.renderPageLinkButton(page, i);
})
}
</div>
);
}
renderPageLinkButton(page, i) {
let { route } = this.props;
let title = _.get(page, 'title.rendered');
let slug = _.get(page, 'slug');
let url = '';
return (
<div className="button" key={i}>
<a href={url}>{title}</a>
</div>
);
}
renderSearchBar() {
let { route } = this.props;
let search = _.get(route.params, 'search', '');
return (
<div className="search-bar">
<span className="input-container">
<input type="text" value="Hello" />
<input type="text" value={search} />
<i className="fa fa-search" />
</span>
</div>
@@ -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,
};

View File

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