mirror of
https://github.com/trambarhq/relaks-wordpress-example.git
synced 2025-09-02 12:42:38 +02:00
Initial check in.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
webpack.debug.js
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:8
|
||||
|
||||
WORKDIR /opt/starwars
|
||||
|
||||
# install dependencies
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
RUN npm install strip-ansi -g && npm install -g npm@6.3 && npm ci --only=production
|
||||
|
||||
# copy code
|
||||
COPY server ./
|
||||
|
||||
EXPOSE 8080
|
||||
CMD node index.js
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Chung Leong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
docker-compose exec varnish varnishstat
|
||||
|
||||
docker-compose exec varnish varnishncsa -F '%h %U%q %{Varnish:hitmiss}x'
|
8480
package-lock.json
generated
Normal file
8480
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "relaks-wordpress-example",
|
||||
"version": "1.0.0",
|
||||
"description": "An example of using Relaks to build an isomorphic WordPress frontend",
|
||||
"scripts": {
|
||||
"watch": "webpack --watch",
|
||||
"build": "webpack",
|
||||
"start": "webpack-dev-server && exit 0",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:trambarhq/relaks-wordpress-example.git"
|
||||
},
|
||||
"author": "Chung Leong",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/trambarhq/relaks-wordpress-example",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.24.1",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-class-properties": "^6.13.0",
|
||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.4.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"css-loader": "^1.0.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"react": "^16.6.3",
|
||||
"regenerator-runtime": "^0.12.0",
|
||||
"relaks": "^1.1.7",
|
||||
"relaks-harvest": "^0.0.2",
|
||||
"relaks-route-manager": "0.0.18",
|
||||
"sass-loader": "^6.0.5",
|
||||
"uglifyjs-webpack-plugin": "^0.4.6",
|
||||
"webpack": "^3.1.0",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"webpack-dev-server": "^2.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-fetch": "^2.2.2",
|
||||
"dnscache": "^1.0.1",
|
||||
"express": "^4.16.3",
|
||||
"express-cache-controller": "^1.1.0",
|
||||
"react-dom": "^16.6.3",
|
||||
"schedule": "^0.5.0",
|
||||
"spider-detector": "^1.0.18"
|
||||
}
|
||||
}
|
105
src/application.jsx
Normal file
105
src/application.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Wordpress from 'wordpress';
|
||||
import { Route } from 'routing';
|
||||
import SideNav from 'widgets/side-nav';
|
||||
import TopNav from 'widgets/top-nav';
|
||||
import 'style.scss';
|
||||
|
||||
class Application extends PureComponent {
|
||||
static displayName = 'Application';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let { routeManager, dataSource } = this.props;
|
||||
this.state = {
|
||||
route: new Route(routeManager),
|
||||
wp: new Wordpress(dataSource, props.ssr),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the application
|
||||
*
|
||||
* @return {VNode}
|
||||
*/
|
||||
render() {
|
||||
let { route, wp } = this.state;
|
||||
let { topNavCollapsed } = this.state;
|
||||
let PageComponent = route.params.module.default;
|
||||
return (
|
||||
<div>
|
||||
<SideNav route={route} wp={wp} />
|
||||
<TopNav route={route} wp={wp} collapsed={topNavCollapsed} />
|
||||
<div className="page-container">
|
||||
<PageComponent route={route} wp={wp} />
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove change handlers when component mounts
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
let { routeManager, dataSource } = this.props;
|
||||
routeManager.removeEventListener('change', this.handleRouteChange);
|
||||
dataSource.removeEventListener('change', this.handleDataSourceChange);
|
||||
document.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
this.setState({ route: new Route(evt.target) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user scrolls the page contents
|
||||
*
|
||||
* @param {Event} evt
|
||||
*/
|
||||
handleScroll = (evt) => {
|
||||
let { topNavCollapsed } = this.state;
|
||||
let container = document.body.parentElement;
|
||||
let previousPos = this.previousScrollPosition || 0;
|
||||
let currentPos = container.scrollTop;
|
||||
let delta = currentPos - previousPos;
|
||||
if (delta > 0) {
|
||||
if (!topNavCollapsed && currentPos > 120) {
|
||||
this.setState({ topNavCollapsed: true });
|
||||
}
|
||||
} else {
|
||||
if (topNavCollapsed) {
|
||||
this.setState({ topNavCollapsed: false });
|
||||
}
|
||||
}
|
||||
this.previousScrollPosition = currentPos;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Application as default,
|
||||
Application
|
||||
};
|
12
src/index.html
Normal file
12
src/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
|
||||
<title>Relaks WordPress Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container"><!--APP--></div>
|
||||
</body>
|
||||
</html>
|
64
src/main.js
Normal file
64
src/main.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createElement } from 'react';
|
||||
import { hydrate, render } from 'react-dom';
|
||||
import { Application } from 'application';
|
||||
import { routes } from 'routing';
|
||||
import WordpressDataSource from 'wordpress-data-source';
|
||||
import RouteManager from 'relaks-route-manager';
|
||||
import { harvest } from 'relaks-harvest';
|
||||
import Relaks from 'relaks';
|
||||
|
||||
const pageBasePath = '/';
|
||||
|
||||
if (typeof(window) === 'object') {
|
||||
async function initialize(evt) {
|
||||
// create data source
|
||||
let host = `${location.protocol}//${location.host}`;
|
||||
let dataSource = new WordpressDataSource({
|
||||
baseURL: `${host}/wp-json`,
|
||||
});
|
||||
dataSource.activate();
|
||||
|
||||
// create route manager
|
||||
let routeManager = new RouteManager({
|
||||
routes,
|
||||
basePath: pageBasePath,
|
||||
preloadingDelay: 2000,
|
||||
});
|
||||
routeManager.activate();
|
||||
await routeManager.start();
|
||||
|
||||
let appContainer = document.getElementById('app-container');
|
||||
if (!appContainer) {
|
||||
throw new Error('Unable to find app element in DOM');
|
||||
}
|
||||
//let ssrElement = createElement(Application, { dataSource, routeManager, ssr: 'hydrate' });
|
||||
//let seeds = await harvest(ssrElement, { seeds: true });
|
||||
//Relaks.set('seeds', seeds);
|
||||
//hydrate(ssrElement, appContainer);
|
||||
|
||||
let appElement = createElement(Application, { dataSource, routeManager });
|
||||
render(appElement, appContainer);
|
||||
}
|
||||
|
||||
window.addEventListener('load', initialize);
|
||||
} else {
|
||||
async function serverSideRender(options) {
|
||||
let dataSource = new WordpressDataSource({
|
||||
baseURL: `${options.host}/wp-json`,
|
||||
fetchFunc: options.fetch,
|
||||
});
|
||||
dataSource.activate();
|
||||
|
||||
let routeManager = new RouteManager({
|
||||
routes,
|
||||
basePath: pageBasePath,
|
||||
});
|
||||
routeManager.activate();
|
||||
await routeManager.start(options.path);
|
||||
|
||||
let ssrElement = createElement(Application, { dataSource, routeManager, ssr: options.target });
|
||||
return harvest(ssrElement);
|
||||
}
|
||||
|
||||
exports.render = serverSideRender;
|
||||
}
|
15
src/pages/category-page.jsx
Normal file
15
src/pages/category-page.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AsyncComponent } from 'relaks';
|
||||
|
||||
class CategoryPage extends AsyncComponent {
|
||||
static displayName = 'CategoryPage';
|
||||
|
||||
async renderAsync(meanwhile) {
|
||||
return <div>Category</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CategoryPage as default,
|
||||
CategoryPage,
|
||||
};
|
15
src/pages/category-story-page.jsx
Normal file
15
src/pages/category-story-page.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AsyncComponent } from 'relaks';
|
||||
|
||||
class CategoryStoryPage extends AsyncComponent {
|
||||
static displayName = 'CategoryStoryPage';
|
||||
|
||||
async renderAsync(meanwhile) {
|
||||
return <div>Category > Story</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CategoryStoryPage as default,
|
||||
CategoryStoryPage,
|
||||
};
|
34
src/pages/welcome-page.jsx
Normal file
34
src/pages/welcome-page.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AsyncComponent } from 'relaks';
|
||||
|
||||
class WelcomePage extends AsyncComponent {
|
||||
static displayName = 'WelcomePage';
|
||||
|
||||
async renderAsync(meanwhile) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome</h1>
|
||||
<p>Lorem ipsum dolor sit amet, utinam quodsi expetendis in has. Primis accumsan mnesarchum ne eam, simul ludus no est, duo ne ferri minim facilisis. Virtute detraxit intellegam quo ad, usu mundi minimum at, soleat insolens intellegam no est. Dicta viderer efficiantur id vix, simul zril legere te sea. Sed quaeque alienum principes ex, ne idque dicit duo, dolor voluptaria in vel.</p>
|
||||
<p>Ne nisl essent cum, at quod antiopam has. Sanctus graecis ocurreret sed at, veniam urbanitas at cum. Epicurei ullamcorper est ut, no mei virtute lobortis indoctum. Putant inermis definiebas ne nec, habeo mazim offendit ei vim. Dicat tempor no duo, mea ex cibo autem. Eos probatus ocurreret rationibus no.</p>
|
||||
<p>Nullam volumus probatus ad mea. Ferri dolores ex nec, purto explicari et pri. Cu luptatum legendos sadipscing cum, fabulas accusamus maiestatis cu sea. In sea vidit accumsan, essent facete persius ex qui. Cum vide aperiri ne, te vix odio elitr labores, ut eum tale eleifend.</p>
|
||||
<p>Mel iudico ancillae eu. Nisl aliquid vulputate sed cu. Harum dolore eloquentiam eum no, ei mazim corpora persecuti ius. Nostrum fastidii eam in, eum at velit alienum blandit. Ei fabulas dolorem maluisset vel, eu mediocrem incorrupte pro. Ut menandri oportere sit. Eam ei vitae posidonium.</p>
|
||||
<p>An sumo iisque posidonium mei, pro ei vide mutat posidonium. Mel meis adolescens intellegam ei, iriure integre dolorem ei sed, duo ea cibo graeci luptatum. Ad vocibus argumentum persequeris his, et mei agam elitr, purto copiosae rationibus pro eu. Duo nostrud perpetua at, antiopam inciderint in vis. Eum et adhuc quaestio iracundia. Illum offendit percipitur id sea, pro numquam verterem comprehensam te, est ex agam mucius.</p>
|
||||
<p>Lorem ipsum dolor sit amet, wisi dicit facete an vim, hinc velit veniam cum ad. Pri inani blandit no, pri virtute fierent petentium at. Nec nihil quodsi id, mutat eruditi nam ei. Cu autem aliquid fabulas mel, an sea sale epicuri deleniti. Ex qui viderer definitionem, quo et vide corrumpit.</p>
|
||||
<p>Ad mei ignota contentiones, ad dicant legendos has. Qui eripuit constituto disputando ne. Qui quem vidit id, nisl nulla mei id. No iudico perfecto patrioque per, et usu repudiare honestatis. An mutat reque impedit sit.</p>
|
||||
<p>Eum prima eruditi ancillae no, pri alia veri nulla ei, at meis volumus vix. Vim ei nemore cetero quaerendum. Eos ad lorem erroribus, ut vidisse scripserit quo. No nostro sensibus dissentias duo. In pri paulo oblique voluptatum, ius habeo veritus an, id ius sumo agam similique. Harum adolescens liberavisse ut sea, id duis tacimates duo. Quo eu alia elitr offendit, eum brute ipsum temporibus ei.</p>
|
||||
<p>Ei maiorum recteque nec. Eum ex habeo quaerendum. Sed tamquam consulatu elaboraret cu. Mea latine offendit singulis et, malis atomorum sensibus ex eum, has vidit laoreet vituperata ei. Et pro quot sonet viderer. Et vero idque delicatissimi qui.</p>
|
||||
<p>Ius quaeque prodesset disputando eu, et veritus nominavi consulatu eum, per ex perfecto praesent. Per in vero sonet, sit at salutandi concludaturque, cu sea tation nominavi mnesarchum. Ea fugit iusto his, mei idque conclusionemque ex. Ea nec omittam deserunt dissentias, iisque maiestatis ius ut, cu mollis voluptua sit. Has graecis placerat pertinacia ut, nihil ornatus eu has. At modus insolens pro, eros dicta cu vel.</p>
|
||||
<p>Ad vis integre constituam sadipscing, magna convenire iudicabit cu nec. Illum apeirian eu mel. Splendide persequeris mel id. Scaevola gloriatur deseruisse cu usu, discere invenire in sit, id viderer eruditi ocurreret quo. Te audire assentior pri, ne pericula neglegentur per. Sint pericula duo ea, feugiat molestie delectus ea eam.</p>
|
||||
<p>Cu harum tation tempor ius, has magna aliquip qualisque ne, quo solet tation omnesque no. Rebum salutatus per ei, mei ferri dolorum ne. Eum ipsum sanctus ei, ei mei voluptua inciderint sadipscing. Eos prompta delenit ei, his dolorum vituperata at. Ne tibique propriae perfecto quo, molestiae prodesset accommodare no eam, mea te ullum appetere. In ludus labores salutandi pri, quo ei nulla forensibus efficiendi, lorem debet facilisi vim no. Ex sea vidit maiestatis.</p>
|
||||
<p>Liber congue eligendi cu mea, in vim veniam ignota. Ex sea quot mollis ocurreret. Ut nihil propriae qualisque vel, et nam delectus mandamus voluptaria. At suas vidit mel.</p>
|
||||
<p>At prodesset consequuntur cum, eum laudem oportere consetetur id. Usu an tation maluisset, at inani aeterno vel, no clita persequeris duo. Fabellas abhorreant ei per, elitr causae usu cu. An sed antiopam salutatus, duo eu postea percipit. Vim quidam habemus contentiones ea, sed essent delectus suavitate te. Assum dicit deleniti sed ei.</p>
|
||||
<p>Everti saperet mea ei, eu per indoctum postulant, tacimates explicari id sea. His saepe voluptaria an, cum vide percipitur dissentiet id. Ei sea natum rebum appareat. Percipit honestatis cu vix. Discere recusabo eu sed, nobis fabellas definitionem ad cum, scribentur consectetuer ea pro. Ea pri simul verear, ridens similique adolescens his eu. Ius reque commune delicata cu, ex eos ferri tractatos, usu vitae habemus patrioque et.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
WelcomePage as default,
|
||||
WelcomePage,
|
||||
};
|
52
src/routing.js
Normal file
52
src/routing.js
Normal file
@@ -0,0 +1,52 @@
|
||||
class Route {
|
||||
constructor(routeManager) {
|
||||
this.routeManager = routeManager;
|
||||
this.name = routeManager.name;
|
||||
this.params = routeManager.params;
|
||||
this.history = routeManager.history;
|
||||
}
|
||||
|
||||
change(url, options) {
|
||||
return this.routeManager.change(url, options);
|
||||
}
|
||||
|
||||
find(name, params) {
|
||||
return this.routeManager.find(name, params);
|
||||
}
|
||||
|
||||
extractID(url) {
|
||||
var si = url.lastIndexOf('/');
|
||||
var ei;
|
||||
if (si === url.length - 1) {
|
||||
ei = si;
|
||||
si = url.lastIndexOf('/', ei - 1);
|
||||
}
|
||||
var text = url.substring(si + 1, ei);
|
||||
return parseInt(text);
|
||||
}
|
||||
}
|
||||
|
||||
let routes = {
|
||||
'welcome-page': {
|
||||
path: '/',
|
||||
load: async (match) => {
|
||||
match.params.module = await import('pages/welcome-page' /* webpackChunkName: "welcome" */);
|
||||
}
|
||||
},
|
||||
'category-page': {
|
||||
path: '/${category}/',
|
||||
params: { category: String },
|
||||
load: async (match) => {
|
||||
match.params.module = await import('pages/category-page' /* webpackChunkName: "category" */);
|
||||
}
|
||||
},
|
||||
'story-page': {
|
||||
path: '/${category}/${slug}/',
|
||||
params: { category: String, slug: String },
|
||||
load: async (match) => {
|
||||
match.params.module = await import('pages/category-story-page' /* webpackChunkName: "category-story" */);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { Route, routes };
|
148
src/style.scss
Normal file
148
src/style.scss
Normal file
@@ -0,0 +1,148 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
BODY {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.contents {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
A {
|
||||
&:link, &:visited {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: #f11;
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
position: fixed;
|
||||
width: 20em;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #66023c;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
position: fixed;
|
||||
left: 20em;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
background-color: #990000;
|
||||
height: 8em;
|
||||
transition: height 0.5s;
|
||||
|
||||
.title {
|
||||
.fa {
|
||||
font-size: 4em;
|
||||
transition: font-size 0.5s, margin-left 0.5s, margin-right 0.5s;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 2em;
|
||||
transition: font-size 0.5s;
|
||||
vertical-align: middle;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #800000;
|
||||
color: #cccccc;
|
||||
height: 1.75em;
|
||||
overflow: hidden;
|
||||
transition: height 0.4s;
|
||||
|
||||
.button {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
border-right: 1px solid transparentize(#cccccc, 0.75);
|
||||
|
||||
&:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #660000;
|
||||
overflow: hidden;
|
||||
height: 1.75em;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
transition: height 0.3s;
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin-right: 0.25em;
|
||||
|
||||
INPUT {
|
||||
max-width: 20em;
|
||||
width: 100%;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
|
||||
.fa-search {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
font-size: 0.9em;
|
||||
color: transparentize(#660000, 0.75);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
.title-bar {
|
||||
height: 1.75em;
|
||||
|
||||
.title {
|
||||
.fa {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-bar, .search-bar {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding-top: 11em;
|
||||
padding-bottom: 1em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
margin-left: 20em;
|
||||
max-width: 60em;
|
||||
}
|
19
src/widgets/side-nav.jsx
Normal file
19
src/widgets/side-nav.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AsyncComponent } from 'relaks';
|
||||
|
||||
class SideNavSync extends PureComponent {
|
||||
static displayName = 'SideNavSync';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="side-nav">
|
||||
Side-Nav
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SideNavSync as default,
|
||||
SideNavSync,
|
||||
};
|
60
src/widgets/top-nav.jsx
Normal file
60
src/widgets/top-nav.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AsyncComponent } from 'relaks';
|
||||
|
||||
class TopBarSync extends PureComponent {
|
||||
static displayName = 'TopNavSync';
|
||||
|
||||
render() {
|
||||
let className = 'top-nav';
|
||||
if (this.props.collapsed) {
|
||||
className += ' collapsed';
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
{this.renderTitleBar()}
|
||||
{this.renderPageLinkBar()}
|
||||
{this.renderSearchBar()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitleBar() {
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<div className="title">
|
||||
<i className="fa fa-home" />
|
||||
<span className="site-name">Romanes eunt domus</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPageLinkBar() {
|
||||
return (
|
||||
<div className="page-bar">
|
||||
<div className="button">
|
||||
Hello
|
||||
</div>
|
||||
<div className="button">
|
||||
World
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSearchBar() {
|
||||
return (
|
||||
<div className="search-bar">
|
||||
<span className="input-container">
|
||||
<input type="text" value="Hello" />
|
||||
<i className="fa fa-search" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
TopBarSync as default,
|
||||
TopBarSync,
|
||||
};
|
2511
src/wordpress-data-source.js
Normal file
2511
src/wordpress-data-source.js
Normal file
File diff suppressed because it is too large
Load Diff
56
src/wordpress.js
Normal file
56
src/wordpress.js
Normal file
@@ -0,0 +1,56 @@
|
||||
class Wordpress {
|
||||
/**
|
||||
* Remember the data source
|
||||
*/
|
||||
constructor(dataSource, ssr) {
|
||||
this.dataSource = dataSource;
|
||||
this.ssr = ssr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one object from data source
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} options
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
fetchOne(url, options) {
|
||||
return this.dataSource.fetchOne(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of objects from data source
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} options
|
||||
*
|
||||
* @return {Promise<Array>}
|
||||
*/
|
||||
fetchList(url, options) {
|
||||
if (this.ssr === 'seo') {
|
||||
options = Object.assign({}, options, { minimum: '100%' });
|
||||
}
|
||||
return this.dataSource.fetchList(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple objects from data source
|
||||
*
|
||||
* @param {Array<String>} urls
|
||||
* @param {Object} options
|
||||
*
|
||||
* @return {Promise<Array>}
|
||||
*/
|
||||
fetchMultiple(urls, options) {
|
||||
if (this.ssr === 'seo') {
|
||||
options = Object.assign({}, options, { minimum: '100%' });
|
||||
}
|
||||
return this.dataSource.fetchMultiple(urls, options);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Wordpress as default,
|
||||
Wordpress,
|
||||
};
|
134
webpack.config.js
Normal file
134
webpack.config.js
Normal file
@@ -0,0 +1,134 @@
|
||||
var FS = require('fs');
|
||||
var Path = require('path');
|
||||
var Webpack = require('webpack');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var DefinePlugin = Webpack.DefinePlugin;
|
||||
var NamedChunksPlugin = Webpack.NamedChunksPlugin;
|
||||
var NamedModulesPlugin = Webpack.NamedModulesPlugin;
|
||||
var UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
|
||||
var event = process.env.npm_lifecycle_event;
|
||||
|
||||
var clientConfig = {
|
||||
context: Path.resolve('./src'),
|
||||
entry: './main',
|
||||
output: {
|
||||
path: Path.resolve('./server/www'),
|
||||
filename: 'app.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: [ '.js', '.jsx' ],
|
||||
modules: [ Path.resolve('./src'), Path.resolve('./node_modules') ],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
presets: [
|
||||
'env',
|
||||
'react',
|
||||
'stage-0',
|
||||
],
|
||||
plugins: [
|
||||
'syntax-async-functions',
|
||||
'syntax-class-properties',
|
||||
'transform-regenerator',
|
||||
'transform-runtime',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
use: 'css-loader!sass-loader',
|
||||
})
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new NamedChunksPlugin,
|
||||
new NamedModulesPlugin,
|
||||
new BundleAnalyzerPlugin({
|
||||
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 = {
|
||||
context: clientConfig.context,
|
||||
entry: clientConfig.entry,
|
||||
target: 'node',
|
||||
output: {
|
||||
path: Path.resolve('./server/client'),
|
||||
filename: 'app.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
},
|
||||
resolve: clientConfig.resolve,
|
||||
module: clientConfig.module,
|
||||
plugins: [
|
||||
new NamedChunksPlugin,
|
||||
new NamedModulesPlugin,
|
||||
new HtmlWebpackPlugin({
|
||||
template: Path.resolve(`./src/index.html`),
|
||||
filename: Path.resolve(`./server/client/index.html`),
|
||||
}),
|
||||
new ExtractTextPlugin('styles.css'),
|
||||
],
|
||||
devtool: clientConfig.devtool,
|
||||
};
|
||||
|
||||
var configs = module.exports = clientConfig; //[ clientConfig, serverConfig ];
|
||||
|
||||
var constants = {};
|
||||
if (event === 'build') {
|
||||
console.log('Optimizing JS code');
|
||||
|
||||
configs.forEach((config) => {
|
||||
// set NODE_ENV to production
|
||||
var plugins = config.plugins;
|
||||
var constants = {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
};
|
||||
plugins.unshift(new DefinePlugin(constants));
|
||||
|
||||
// use Uglify to remove dead-code
|
||||
plugins.unshift(new UglifyJSPlugin({
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
// copy webpack.resolve.js into webpack.debug.js to resolve Babel presets
|
||||
// and plugins to absolute paths, required when linked modules are used
|
||||
if (FS.existsSync('./webpack.debug.js')) {
|
||||
configs.map(require('./webpack.debug.js'));
|
||||
}
|
30
webpack.resolve.js
Normal file
30
webpack.resolve.js
Normal file
@@ -0,0 +1,30 @@
|
||||
var Path = require('path');
|
||||
|
||||
module.exports = function(config) {
|
||||
config.module.rules.forEach((rule) => {
|
||||
if (rule.loader === 'babel-loader' && rule.query) {
|
||||
if (rule.query.presets) {
|
||||
rule.query.presets = rule.query.presets.map((preset) => {
|
||||
return resolve('preset', preset);
|
||||
})
|
||||
}
|
||||
if (rule.query.plugins) {
|
||||
rule.query.plugins = rule.query.plugins.map((plugin) => {
|
||||
return resolve('plugin', plugin);
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
function resolve(type, module) {
|
||||
if (module instanceof Array) {
|
||||
module[0] = resolve(type, module[0]);
|
||||
return module;
|
||||
} else {
|
||||
if (!/^[\w\-]+$/.test(module)) {
|
||||
return module;
|
||||
}
|
||||
return Path.resolve(`./node_modules/babel-${type}-${module}`);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user