├── .gitignore
├── assets
├── font
│ ├── fa.eot
│ ├── fa.ttf
│ ├── fa.woff
│ ├── fa.woff2
│ └── fa.svg
├── images
│ ├── favicon.ico
│ ├── profile-1.jpg
│ ├── react_logo.png
│ ├── default-about.jpg
│ ├── default-contact.jpg
│ └── default-sidebar.jpg
├── js
│ ├── vendors
│ │ ├── redux-thunk.js
│ │ ├── recompose.js
│ │ ├── lodash.debounce.js
│ │ ├── redux.js
│ │ ├── prop-types.js
│ │ ├── react-helmet.js
│ │ └── history.js
│ └── updater.js
├── build
│ ├── conf.js
│ ├── index_prod.html
│ └── index_dev.html
├── html
│ ├── 404_no_root.html
│ ├── 404_with_root.html
│ ├── index_prod.html
│ └── index_dev.html
└── css
│ ├── fa.css
│ └── bootstrap-reboot.css
├── src
├── modules
│ ├── article
│ │ ├── actionTypes.js
│ │ ├── selectors.js
│ │ ├── actionCreators.js
│ │ └── reducer.js
│ ├── category
│ │ ├── selectors.js
│ │ ├── actionTypes.js
│ │ ├── reducer.js
│ │ └── actionCreators.js
│ ├── route
│ │ ├── actionTypes.js
│ │ ├── middleware.js
│ │ ├── reducer.js
│ │ ├── selectors.js
│ │ ├── ConnectedRouter.js
│ │ └── actionCreators.js
│ └── main
│ │ ├── rootReducer.js
│ │ ├── containers
│ │ ├── noMatch.js
│ │ └── root.js
│ │ └── store
│ │ └── configureStore.js
├── utils
│ ├── capitalize.js
│ ├── jsonpCall.js
│ └── uuid.js
├── app.js
├── routes
│ ├── routes.js
│ ├── about.js
│ ├── home.js
│ ├── category.js
│ ├── article.js
│ └── contact.js
├── lib
│ ├── mail.js
│ ├── api.js
│ └── drive.js
├── styles
│ ├── input.js
│ ├── blocks.js
│ └── buttons.js
└── components
│ ├── disqus
│ ├── disqusCount.js
│ └── disqusThread.js
│ ├── blocks
│ ├── category.js
│ └── article.js
│ ├── form
│ └── baseInput.js
│ └── layout
│ ├── sidebar.js
│ ├── page.js
│ ├── menu.js
│ └── footer.js
├── conf.json
├── dev
├── middleware
│ ├── transform-middleware.js
│ ├── send.js
│ ├── cache.js
│ ├── transform-file.js
│ ├── update.js
│ ├── resolveToUrl.js
│ └── update-middleware.js
├── dev-server.js
└── build
│ └── build-app.js
├── .babelrc
├── README.md
├── LICENSE
├── package.json
├── 404.html
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 |
4 |
--------------------------------------------------------------------------------
/assets/font/fa.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/font/fa.eot
--------------------------------------------------------------------------------
/assets/font/fa.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/font/fa.ttf
--------------------------------------------------------------------------------
/assets/font/fa.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/font/fa.woff
--------------------------------------------------------------------------------
/assets/font/fa.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/font/fa.woff2
--------------------------------------------------------------------------------
/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/favicon.ico
--------------------------------------------------------------------------------
/assets/images/profile-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/profile-1.jpg
--------------------------------------------------------------------------------
/assets/images/react_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/react_logo.png
--------------------------------------------------------------------------------
/assets/images/default-about.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/default-about.jpg
--------------------------------------------------------------------------------
/assets/images/default-contact.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/default-contact.jpg
--------------------------------------------------------------------------------
/assets/images/default-sidebar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/misterfresh/react-without-webpack/HEAD/assets/images/default-sidebar.jpg
--------------------------------------------------------------------------------
/src/modules/article/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_ARTICLE = 'REQUEST_ARTICLE'
2 | export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE'
3 |
--------------------------------------------------------------------------------
/src/modules/category/selectors.js:
--------------------------------------------------------------------------------
1 | export const shouldFetchCategories = state =>
2 | !state.category.isFetching && !state.category.fetched
3 |
--------------------------------------------------------------------------------
/src/modules/category/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_CATEGORIES = 'REQUEST_CATEGORIES'
2 | export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES'
3 |
--------------------------------------------------------------------------------
/src/modules/route/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
2 | export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
3 |
--------------------------------------------------------------------------------
/src/utils/capitalize.js:
--------------------------------------------------------------------------------
1 | export default function capitalize(string) {
2 | if (!string) {
3 | return ''
4 | }
5 | return string.charAt(0).toUpperCase() + string.slice(1)
6 | }
7 |
--------------------------------------------------------------------------------
/assets/js/vendors/redux-thunk.js:
--------------------------------------------------------------------------------
1 | function createThunkMiddleware(t){return({dispatch:e,getState:n})=>r=>u=>"function"==typeof u?u(e,n,t):r(u)}const thunk=createThunkMiddleware();thunk.withExtraArgument=createThunkMiddleware;export default thunk;
--------------------------------------------------------------------------------
/assets/js/updater.js:
--------------------------------------------------------------------------------
1 | let source = new EventSource('/stream')
2 |
3 | source.addEventListener(
4 | 'message',
5 | function(event) {
6 | console.log('updating!', event.data)
7 | location.reload(true)
8 | },
9 | false
10 | )
11 |
--------------------------------------------------------------------------------
/assets/build/conf.js:
--------------------------------------------------------------------------------
1 | export default {"author":"React Drive CMS","dashboardId":"1-on_GfmvaEcOk7HcWfKb8B6KFRv166RkLN2YmDEtDn4","sendContactMessageUrlId":"AKfycbyL4vW1UWs4mskuDjLoLmf1Hjan1rTLEca6i2Hi2H_4CtKUN84d","shortname":"easydrivecms","root":"react-drive-cms"}
--------------------------------------------------------------------------------
/conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "React Drive CMS",
3 | "dashboardId": "1-on_GfmvaEcOk7HcWfKb8B6KFRv166RkLN2YmDEtDn4",
4 | "sendContactMessageUrlId":
5 | "AKfycbyL4vW1UWs4mskuDjLoLmf1Hjan1rTLEca6i2Hi2H_4CtKUN84d",
6 | "shortname": "easydrivecms",
7 | "root": "react-drive-cms"
8 | }
9 |
--------------------------------------------------------------------------------
/dev/middleware/transform-middleware.js:
--------------------------------------------------------------------------------
1 | let transformFile = require('./transform-file')
2 |
3 | function transformMiddleware(req, res, next) {
4 | if (req.url.startsWith('/src/')) {
5 | return transformFile(req, res, next)
6 | } else {
7 | return next()
8 | }
9 | }
10 |
11 | module.exports = transformMiddleware
12 |
--------------------------------------------------------------------------------
/src/modules/main/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import route from 'route/reducer'
4 | import article from 'article/reducer'
5 | import category from 'category/reducer'
6 |
7 | const rootReducer = combineReducers({
8 | article,
9 | category,
10 | route
11 | })
12 |
13 | export default rootReducer
14 |
--------------------------------------------------------------------------------
/src/modules/main/containers/noMatch.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import { Helmet } from 'react-helmet'
3 |
4 | class NoMatch extends PureComponent {
5 | render() {
6 | return (
7 |
8 |
9 | Page was not found
10 |
11 | )
12 | }
13 | }
14 |
15 | export default NoMatch
16 |
--------------------------------------------------------------------------------
/src/modules/main/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 |
4 | import rootReducer from 'main/rootReducer'
5 |
6 | export default function configureStore(initialState, routerMiddleware) {
7 | return createStore(
8 | rootReducer,
9 | initialState,
10 | applyMiddleware(thunk, routerMiddleware)
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/main/containers/root.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Provider } from 'react-redux'
3 | import ConnectedRouter from 'route/ConnectedRouter'
4 | import { renderRoutes } from 'react-router-dom'
5 | import { css } from 'aphrodite'
6 | import routes from 'routes/routes'
7 |
8 | import blocks from 'styles/blocks'
9 |
10 | let Root = ({ store, history }) => (
11 |
12 |
13 | {renderRoutes(routes)}
14 |
15 |
16 | )
17 |
18 | export default Root
19 |
--------------------------------------------------------------------------------
/src/modules/article/selectors.js:
--------------------------------------------------------------------------------
1 | export const shouldFetchArticle = (state, articleId) => {
2 | if (!articleId) {
3 | return false
4 | }
5 | return (
6 | !articleIsLoading(state, articleId) &&
7 | typeof state.article.texts[articleId] === 'undefined'
8 | )
9 | }
10 |
11 | export const articleIsLoading = (state, articleId) => {
12 | if (!articleId) {
13 | return false
14 | }
15 | return state.article.isFetching[articleId]
16 | }
17 |
18 | export const articleIsFetched = (state, articleId) => {
19 | if (!articleId) {
20 | return false
21 | }
22 | return state.article.fetched[articleId]
23 | }
24 |
--------------------------------------------------------------------------------
/src/modules/route/middleware.js:
--------------------------------------------------------------------------------
1 | import { CALL_HISTORY_METHOD } from './actionTypes'
2 |
3 | /**
4 | * This middleware captures CALL_HISTORY_METHOD actions to redirect to the
5 | * provided history object. This will prevent these actions from reaching your
6 | * reducer or any middleware that comes after this one.
7 | */
8 | export default function routerMiddleware(history) {
9 | return () => next => action => {
10 | if (action.type !== CALL_HISTORY_METHOD) {
11 | return next(action)
12 | }
13 | const {
14 | location: { method, args }
15 | } = action
16 | history[method](...args)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/dev/middleware/send.js:
--------------------------------------------------------------------------------
1 | function send(
2 | res,
3 | transpiled,
4 | fromCache = false,
5 | contentType = 'application/javascript'
6 | ) {
7 | res.setHeader('Content-type', contentType)
8 | res.setHeader('Content-Encoding', 'gzip')
9 | res.setHeader('Accept-Ranges', 'bytes')
10 | res.setHeader(
11 | 'Cache-Control',
12 | 'private, no-cache, no-store, must-revalidate'
13 | )
14 | res.setHeader('Expires', '-1')
15 | res.setHeader('Pragma', 'no-cache')
16 | res.setHeader('Connection', 'keep-alive')
17 | res.append('X-App-Cache-Hit', fromCache)
18 | res.status(200).send(transpiled)
19 | }
20 |
21 | module.exports = send
22 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react'
3 |
4 | import { createBrowserHistory } from 'history'
5 | import { StyleSheet } from 'aphrodite'
6 |
7 | import routerMiddleware from 'route/middleware'
8 |
9 | import Root from 'main/containers/root'
10 | import configureStore from 'main/store/configureStore'
11 |
12 | let initialState = {}
13 | import conf from 'conf'
14 | let history = createBrowserHistory(conf.root ? { basename: conf.root } : {})
15 |
16 | const store = configureStore(initialState, routerMiddleware(history))
17 |
18 | render(
19 | ,
20 | document.getElementById('app-mount'),
21 | document.getElementById('app-mount').firstElementChild
22 | )
23 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/react"],
3 | "plugins": [
4 | ["@babel/plugin-proposal-class-properties"],
5 | ["@babel/plugin-syntax-object-rest-spread"],
6 | ["module-resolver", {
7 | "root": ["./"],
8 | "alias": {
9 | "conf": "./conf.js",
10 | "components": "./src/components",
11 | "article": "./src/modules/article",
12 | "category": "./src/modules/category",
13 | "contact": "./src/modules/contact",
14 | "display": "./src/modules/display",
15 | "lib": "./src/lib",
16 | "main": "./src/modules/main",
17 | "route": "./src/modules/route",
18 | "routes": "./src/routes",
19 | "styles": "./src/styles",
20 | "utils": "./src/utils"
21 | }
22 | }]
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/dev/middleware/cache.js:
--------------------------------------------------------------------------------
1 | let cache = {
2 | cacheMap: {},
3 | hashMap: {},
4 | stale: {},
5 | links: {}
6 | }
7 |
8 | function set(store, key, value) {
9 | cache[store][key] = value
10 | }
11 |
12 | function remove(store, key) {
13 | delete cache[store][key]
14 | }
15 |
16 | function get(store, key) {
17 | if (typeof cache[store][key] === 'undefined') {
18 | return false
19 | }
20 | return cache[store][key]
21 | }
22 |
23 | function getAll(store) {
24 | return cache[store]
25 | }
26 |
27 | function dump() {
28 | console.log('hash map', JSON.stringify(cache.hashMap))
29 | console.log('cache map', JSON.stringify(Object.keys(cache.cacheMap)))
30 | }
31 |
32 | module.exports = {
33 | set,
34 | remove,
35 | get,
36 | getAll,
37 | dump
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/category/reducer.js:
--------------------------------------------------------------------------------
1 | import { REQUEST_CATEGORIES, RECEIVE_CATEGORIES } from './actionTypes'
2 |
3 | export default function category(state = initialState, action) {
4 | switch (action.type) {
5 | case REQUEST_CATEGORIES:
6 | return {
7 | ...state,
8 | isFetching: true
9 | }
10 |
11 | case RECEIVE_CATEGORIES:
12 | return {
13 | ...state,
14 | isFetching: true,
15 | fetched: true,
16 | categories: {
17 | ...action.categories.categories
18 | }
19 | }
20 |
21 | default:
22 | return state
23 | }
24 | }
25 |
26 | const initialState = {
27 | isFetching: false,
28 | fetched: false,
29 | categories: {}
30 | }
31 |
--------------------------------------------------------------------------------
/src/routes/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Article from './article'
4 | import Category from './category'
5 |
6 | import About from './about'
7 | import Contact from './contact'
8 | import Home from './home'
9 |
10 | import NoMatch from 'main/containers/noMatch'
11 |
12 | export default [
13 | {
14 | path: '/',
15 | component: Home,
16 | exact: true
17 | },
18 | {
19 | path: '/articles/:articleId/:slug',
20 | component: Article
21 | },
22 | {
23 | path: '/categories/:categoryId',
24 | component: Category
25 | },
26 | {
27 | path: '/about',
28 | component: About,
29 | exact: true
30 | },
31 | {
32 | path: '/contact',
33 | component: Contact,
34 | exact: true
35 | },
36 | {
37 | path: '/*',
38 | component: NoMatch
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/src/utils/jsonpCall.js:
--------------------------------------------------------------------------------
1 | export default function jsonpCall(url, callback) {
2 | let handleJsonpResults =
3 | 'handleJsonpResults_' +
4 | Date.now() +
5 | '_' +
6 | parseInt(Math.random() * 10000)
7 |
8 | window[handleJsonpResults] = function(json) {
9 | callback(json)
10 |
11 | const script = document.getElementById(handleJsonpResults)
12 | document.getElementsByTagName('head')[0].removeChild(script)
13 | delete window[handleJsonpResults]
14 | }
15 |
16 | let serviceUrl = `${url}${
17 | url.indexOf('?') > -1 ? '&' : '?'
18 | }callback=${handleJsonpResults}`
19 | //console.log('service', serviceUrl)
20 | const jsonpScript = document.createElement('script')
21 | jsonpScript.setAttribute('src', serviceUrl)
22 | jsonpScript.id = handleJsonpResults
23 | document.getElementsByTagName('head')[0].appendChild(jsonpScript)
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/mail.js:
--------------------------------------------------------------------------------
1 | import Api from './api'
2 | import conf from 'conf'
3 | import jsonpCall from 'utils/jsonpCall'
4 |
5 | class Mail extends Api {
6 | constructor() {
7 | super()
8 | this.send = this.send.bind(this)
9 | }
10 |
11 | send(form) {
12 | jsonpCall('http://smart-ip.net/info-json', ipInfo => {
13 | form.ip = ipInfo.address
14 | form.country = ipInfo.countryName
15 |
16 | jsonpCall(
17 | `https://script.google.com/macros/s/${
18 | conf.sendContactMessageUrlId
19 | }/exec?${Object.keys(form)
20 | .map(
21 | property =>
22 | `${property}=${encodeURIComponent(form[property])}`
23 | )
24 | .join('&')}`,
25 | response => {
26 | console.log(response)
27 | }
28 | )
29 | })
30 | }
31 | }
32 |
33 | export default new Mail()
34 |
--------------------------------------------------------------------------------
/src/utils/uuid.js:
--------------------------------------------------------------------------------
1 | let lut = []
2 | for (let i = 0; i < 256; i++) {
3 | lut[i] = (i < 16 ? '0' : '') + i.toString(16)
4 | }
5 |
6 | function uuid() {
7 | let d0 = (Math.random() * 0xffffffff) | 0
8 | let d1 = (Math.random() * 0xffffffff) | 0
9 | let d2 = (Math.random() * 0xffffffff) | 0
10 | let d3 = (Math.random() * 0xffffffff) | 0
11 | return (
12 | lut[d0 & 0xff] +
13 | lut[(d0 >> 8) & 0xff] +
14 | lut[(d0 >> 16) & 0xff] +
15 | lut[(d0 >> 24) & 0xff] +
16 | '-' +
17 | lut[d1 & 0xff] +
18 | lut[(d1 >> 8) & 0xff] +
19 | '-' +
20 | lut[((d1 >> 16) & 0x0f) | 0x40] +
21 | lut[(d1 >> 24) & 0xff] +
22 | '-' +
23 | lut[(d2 & 0x3f) | 0x80] +
24 | lut[(d2 >> 8) & 0xff] +
25 | '-' +
26 | lut[(d2 >> 16) & 0xff] +
27 | lut[(d2 >> 24) & 0xff] +
28 | lut[d3 & 0xff] +
29 | lut[(d3 >> 8) & 0xff] +
30 | lut[(d3 >> 16) & 0xff] +
31 | lut[(d3 >> 24) & 0xff]
32 | )
33 | }
34 |
35 | export default uuid
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React without Webpack
2 |
3 | The lightest React Starter Kit. Using native modules, we can avoid bundling and have instant reloading during development.
4 |
5 | 
6 |
7 | ### Usage
8 | #### 1. Installation
9 | Clone the project and install dependencies.
10 | ```bash
11 | git clone git@github.com:misterfresh/react-without-webpack.git
12 | cd react-without-webpack
13 | npm install
14 | ```
15 | #### 2. Development
16 | Start the development server, edit the code in the /src folder.
17 | ```bash
18 | npm run dev
19 | ```
20 | #### 3. Deploy
21 | Build the project:
22 | ```bash
23 | npm run build
24 | ```
25 | Run the built project:
26 | ```bash
27 | npm run local
28 | ```
29 | Deploy to GitHub Pages by pushing to a "gh-pages" branch.
30 |
31 | ### How it works
32 | Some details in this medium article : [React without Webpack](https://medium.com/@antoine.stollsteiner/react-without-webpack-a-dream-come-true-6cf24a1ff766)
33 |
34 | Live demo here:
35 | http://misterfresh.github.io/react-drive-cms/
36 |
--------------------------------------------------------------------------------
/src/modules/category/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { REQUEST_CATEGORIES, RECEIVE_CATEGORIES } from './actionTypes'
2 | import { shouldFetchCategories } from './selectors'
3 | import Drive from 'lib/drive'
4 |
5 | export function fetchCategoriesIfNeeded() {
6 | return (dispatch, getState) => {
7 | let state = getState()
8 |
9 | if (shouldFetchCategories(state)) {
10 | return fetchCategories(dispatch)
11 | }
12 | }
13 | }
14 |
15 | export function fetchCategories(dispatch) {
16 | dispatch(requestCategories())
17 | return Drive.getCategories()
18 | .then(categories => {
19 | return dispatch(receiveCategories(categories))
20 | })
21 | .catch(function(error) {
22 | console.log('code:', error.code, ' message:', error.message)
23 | })
24 | }
25 |
26 | export function requestCategories() {
27 | return {
28 | type: REQUEST_CATEGORIES
29 | }
30 | }
31 |
32 | export function receiveCategories(categories) {
33 | return {
34 | type: RECEIVE_CATEGORIES,
35 | categories
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/input.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'aphrodite'
2 |
3 | let input = StyleSheet.create({
4 | base: {
5 | display: 'block',
6 | width: '100%',
7 | padding: '.5rem .75rem',
8 | fontSize: '1.6rem',
9 | lineHeight: '1.25',
10 | color: '#464a4c',
11 | backgroundColor: '#fff',
12 | backgroundImage: 'none',
13 | backgroundClip: 'padding-box',
14 | border: '.1rem solid rgba(0,0,0,.15)',
15 | borderRadius: '.25rem',
16 | transition:
17 | 'border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s',
18 | marginBottom: 10,
19 | '::placeholder': {
20 | maxWidth: '92%',
21 | overflowX: 'hidden',
22 | textOverflow: 'ellipsis'
23 | }
24 | },
25 | error: {
26 | border: '.1rem solid red',
27 | '::placeholder': {
28 | maxWidth: '92%',
29 | overflowX: 'hidden',
30 | textOverflow: 'ellipsis',
31 | color: 'red'
32 | }
33 | }
34 | })
35 |
36 | export default input
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Antoine S
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/modules/article/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { REQUEST_ARTICLE, RECEIVE_ARTICLE } from './actionTypes'
2 | import { shouldFetchArticle } from './selectors'
3 | import Drive from 'lib/drive'
4 |
5 | export function fetchArticleIfNeeded(articleId) {
6 | return (dispatch, getState) => {
7 | let state = getState()
8 |
9 | if (shouldFetchArticle(state, articleId)) {
10 | return fetchArticle(dispatch, articleId)
11 | }
12 | }
13 | }
14 |
15 | export function fetchArticle(dispatch, articleId) {
16 | dispatch(requestArticle(articleId))
17 | return Drive.getArticleHtml(articleId)
18 | .then(article => {
19 | return dispatch(receiveArticle(articleId, article))
20 | })
21 | .catch(function(error) {
22 | console.log('code:', error.code, ' message:', error.message)
23 | })
24 | }
25 |
26 | export function requestArticle(articleId) {
27 | return {
28 | type: REQUEST_ARTICLE,
29 | articleId
30 | }
31 | }
32 |
33 | export function receiveArticle(articleId, article) {
34 | return {
35 | type: RECEIVE_ARTICLE,
36 | articleId,
37 | article
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/lib/api.js:
--------------------------------------------------------------------------------
1 | export default class Api {
2 | constructor() {
3 | this.call = this.call.bind(this)
4 | this.get = this.get.bind(this)
5 | this.post = this.post.bind(this)
6 | }
7 |
8 | call(
9 | url,
10 | options = {
11 | method: 'GET',
12 | credentials: 'include',
13 | headers: {}
14 | }
15 | ) {
16 | let { method, credentials, headers } = options
17 |
18 | if (!credentials) {
19 | options = { ...options, credentials: 'include' }
20 | }
21 |
22 | options = Object.assign({}, options, {
23 | headers
24 | })
25 |
26 | return fetch(url, options)
27 | }
28 |
29 | get(
30 | url,
31 | options = {
32 | method: 'GET',
33 | credentials: 'include'
34 | }
35 | ) {
36 | return this.call(url, { ...options, method: 'GET' })
37 | }
38 |
39 | post(
40 | url,
41 | options = {
42 | method: 'POST',
43 | credentials: 'include'
44 | }
45 | ) {
46 | return this.call(url, { ...options, method: 'POST' })
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/assets/js/vendors/recompose.js:
--------------------------------------------------------------------------------
1 | import{default as React,Component}from "./react.js";let createFactory=React.createFactory;const hasOwnProperty=Object.prototype.hasOwnProperty;export function is(e, t){return e===t?0!==e||0!==t||1/e==1/t:e!==e&&t!==t};export function shallowEqual(e, t){if(is(e,t))return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;const r=Object.keys(e),o=Object.keys(t);if(r.length!==o.length)return!1;for(let o=0; o r=>(r[e]=t,r);export const setDisplayName=e=>setStatic("displayName",e);export const getDisplayName=e=>{if("string"==typeof e)return e;if(e)return e.displayName||e.name||"Component"};const wrapDisplayName=(e,t)=>`${t}(${getDisplayName(e)})`;export const shouldUpdate=e=>t=>{const r=createFactory(t);class o extends Component{shouldComponentUpdate(t){return e(this.props,t)}render(){return r(this.props)}}return setDisplayName(wrapDisplayName(t,"shouldUpdate"))(o)};export const pure=e=>{const t=shouldUpdate((e,t)=>!shallowEqual(e,t));return setDisplayName(wrapDisplayName(e,"pure"))(t(e))};export function compose(...e){return 0===e.length?e=>e:1===e.length?e[0]:e.reduce((e,t)=>(...r)=>e(t(...r)))};
2 |
--------------------------------------------------------------------------------
/src/modules/route/reducer.js:
--------------------------------------------------------------------------------
1 | import { LOCATION_CHANGE } from './actionTypes'
2 |
3 | const initialState = {
4 | location: {
5 | pathname: '/',
6 | query: {},
7 | search: ''
8 | }
9 | }
10 |
11 | export default function route(state = initialState, action) {
12 | //console.log('route reducer')
13 | switch (action.type) {
14 | case LOCATION_CHANGE:
15 | let location = { ...action.location }
16 | if (
17 | !!location &&
18 | !!location.search &&
19 | (!location.query || !Object.keys(location.query).length)
20 | ) {
21 | let search = location.search.slice(1)
22 | location.query = JSON.parse(
23 | '{"' +
24 | decodeURI(search.replace('+', ' '))
25 | .replace(/"/g, '\\"')
26 | .replace(/&/g, '","')
27 | .replace(/=/g, '":"') +
28 | '"}'
29 | )
30 | }
31 | return {
32 | ...state,
33 | location
34 | }
35 |
36 | default:
37 | return state
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/modules/route/selectors.js:
--------------------------------------------------------------------------------
1 | import { matchPath } from 'react-router-dom'
2 |
3 | export const getLocation = state => state.route.location
4 |
5 | export const createMatchSelector = path => {
6 | let lastPathname = null
7 | let lastMatch = null
8 | return state => {
9 | const { pathname } = getLocation(state)
10 | if (pathname === lastPathname) {
11 | return lastMatch
12 | }
13 | lastPathname = pathname
14 | const match = matchPath(pathname, path)
15 | if (!match || !lastMatch || match.url !== lastMatch.url) {
16 | lastMatch = match
17 | }
18 | return lastMatch
19 | }
20 | }
21 |
22 | export const getQuery = state =>
23 | !!state.route.location.query ? state.route.location.query : {}
24 |
25 | export const getSerializedQuery = state => state.route.location.search.slice(1)
26 |
27 | export const getPath = state => state.route.location.pathname
28 |
29 | export const getPathParts = state =>
30 | state.route.location.pathname.split('/').filter(part => !!part)
31 |
32 | export const getRoot = state => {
33 | let parts = getPathParts(state)
34 | let root = 'home'
35 | if (typeof parts[0] !== 'undefined') {
36 | root = parts[0]
37 | }
38 | return root
39 | }
40 |
--------------------------------------------------------------------------------
/src/modules/route/ConnectedRouter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Router } from 'react-router-dom'
4 | import { LOCATION_CHANGE } from './actionTypes'
5 |
6 | class ConnectedRouter extends Component {
7 | static propTypes = {
8 | store: PropTypes.object,
9 | history: PropTypes.object,
10 | children: PropTypes.node
11 | }
12 |
13 | static contextTypes = {
14 | store: PropTypes.object
15 | }
16 |
17 | handleLocationChange = location => {
18 | this.store.dispatch({
19 | type: LOCATION_CHANGE,
20 | location
21 | })
22 | }
23 |
24 | componentWillMount() {
25 | const { store: propsStore, history } = this.props
26 | this.store = propsStore || this.context.store
27 | this.handleLocationChange(history.location)
28 | }
29 |
30 | componentDidMount() {
31 | const { history } = this.props
32 | this.unsubscribeFromHistory = history.listen(this.handleLocationChange)
33 | }
34 |
35 | componentWillUnmount() {
36 | if (this.unsubscribeFromHistory) this.unsubscribeFromHistory()
37 | }
38 |
39 | render() {
40 | return
41 | }
42 | }
43 |
44 | export default ConnectedRouter
45 |
--------------------------------------------------------------------------------
/dev/middleware/transform-file.js:
--------------------------------------------------------------------------------
1 | let path = require('path')
2 | let fs = require('fs')
3 | let crypto = require('crypto')
4 | let cache = require('./cache')
5 | let update = require('./update')
6 | let send = require('./send')
7 |
8 | function transformFile(req, res, next) {
9 | let uri = req.url.split('?').shift()
10 | let file = path.join(process.cwd(), uri)
11 | let src = file.replace(/\\/gi, '/')
12 |
13 | fs.lstat(file, function(err, stats) {
14 | if (err) {
15 | res.status(500).json(err)
16 | } else {
17 | let mtime = stats.mtime.getTime()
18 | let lastModifiedHash = crypto
19 | .createHash('md5')
20 | .update(mtime + '-' + src)
21 | .digest('hex')
22 |
23 | let lastKnownHash = cache.get('hashMap', src)
24 | if (lastKnownHash && lastKnownHash === lastModifiedHash) {
25 | send(res, cache.get('cacheMap', lastKnownHash), true)
26 | } else {
27 | update(file, lastModifiedHash, function(err, updated) {
28 | if (err) {
29 | res.status(500).json(err)
30 | } else {
31 | send(res, updated.file)
32 | }
33 | })
34 | }
35 | }
36 | })
37 | }
38 |
39 | module.exports = transformFile
40 |
--------------------------------------------------------------------------------
/src/styles/blocks.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'aphrodite'
2 |
3 | const opacityKeyframes = {
4 | from: {
5 | opacity: 0
6 | },
7 |
8 | to: {
9 | opacity: 1
10 | }
11 | }
12 | let blocks = StyleSheet.create({
13 | outline: {
14 | border: '0.1rem solid transparent'
15 | },
16 | image: {
17 | backgroundSize: 'cover',
18 | backgroundRepeat: 'no-repeat',
19 | backgroundPosition: 'center center'
20 | },
21 | center: {
22 | width: '30%',
23 | margin: 'auto'
24 | },
25 | container: {
26 | padding: '0.5rem'
27 | },
28 | col: {
29 | flexGrow: 1
30 | },
31 | row: {
32 | display: 'flex'
33 | },
34 | wrapper: {
35 | position: 'relative',
36 | top: 0,
37 | bottom: 0,
38 | left: 0,
39 | right: 0,
40 | width: '100%',
41 | height: '100%',
42 | margin: 0,
43 | padding: 0,
44 | overflowX: 'hidden',
45 | maxWidth: '100%'
46 | },
47 | block: {
48 | background: '#fff',
49 | padding: 10,
50 | borderRadius: 4,
51 | position: 'relative',
52 | cursor: 'default'
53 | },
54 | fadein: {
55 | animationName: [opacityKeyframes],
56 | animationDuration: '1s, 1s',
57 | animationIterationCount: 1
58 | }
59 | })
60 |
61 | export default blocks
62 |
--------------------------------------------------------------------------------
/dev/middleware/update.js:
--------------------------------------------------------------------------------
1 | let path = require('path')
2 | let zlib = require('zlib')
3 | let babel = require('@babel/core')
4 |
5 | let cache = require('./cache')
6 | let resolveToUrl = require('./resolveToUrl')
7 | let conf = JSON.parse(
8 | require('fs').readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8')
9 | )
10 | conf.babelrc = false
11 | conf.plugins.pop()
12 | conf.plugins.push([resolveToUrl])
13 |
14 | function update(file, hash, cb) {
15 | babel.transformFile(file, conf, function(err, transpiled) {
16 | if (err) {
17 | cb(err)
18 | } else {
19 | zlib.gzip(transpiled.code, function(err, gzipped) {
20 | if (err) {
21 | cb(err)
22 | } else {
23 | let path = file.replace(/\\/gi, '/')
24 |
25 | cache.remove('cacheMap', cache.get('hashMap', path))
26 | cache.set('hashMap', path, hash)
27 | cache.set('cacheMap', hash, gzipped)
28 |
29 | let link = '/src/' + path.split('/src/')[1]
30 | let cached = {
31 | file: gzipped,
32 | type: 'application/javascript'
33 | }
34 | cache.set('links', link, cached)
35 |
36 | cb(null, cached)
37 | }
38 | })
39 | }
40 | })
41 | }
42 |
43 | module.exports = update
44 |
--------------------------------------------------------------------------------
/src/styles/buttons.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'aphrodite'
2 |
3 | let buttons = StyleSheet.create({
4 | base: {
5 | display: 'inline-block',
6 | fontWeight: '400',
7 | lineHeight: '1.25rem',
8 | textAlign: 'center',
9 | whiteSpace: 'nowrap',
10 | verticalAlign: 'middle',
11 | userSelect: 'none',
12 | border: '0.1rem solid transparent',
13 | padding: '1rem 1rem',
14 | fontSize: '1.6rem',
15 | borderRadius: '.5rem',
16 | transition: 'all .2s ease-in-out',
17 | cursor: 'pointer',
18 | textDecoration: 'none',
19 | color: '#fff',
20 | backgroundColor: '#0275d8',
21 | borderColor: '#0275d8',
22 | ':hover': {
23 | textDecoration: 'none',
24 | backgroundColor: '#025aa5',
25 | borderColor: '#01549b',
26 | color: '#fff'
27 | }
28 | },
29 | large: {
30 | padding: '1.5rem',
31 | fontSize: '2rem',
32 | borderRadius: '.5rem'
33 | },
34 | block: {
35 | display: 'block',
36 | width: '100%'
37 | },
38 | disabled: {
39 | pointerEvents: 'none',
40 | backgroundColor: '#85c6f2',
41 | borderColor: '#85c6f2',
42 | ':hover': {
43 | backgroundColor: '#85c6f2',
44 | borderColor: '#85c6f2',
45 | cursor: 'not-allowed'
46 | }
47 | }
48 | })
49 |
50 | export default buttons
51 |
--------------------------------------------------------------------------------
/src/components/disqus/disqusCount.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import conf from 'conf'
4 |
5 | export default class DisqusCount extends Component {
6 | constructor(props) {
7 | super(props)
8 | this.addDisqusScript = this.addDisqusScript.bind(this)
9 | this.removeDisqusScript = this.removeDisqusScript.bind(this)
10 | }
11 |
12 | addDisqusScript() {
13 | this.disqusCount = document.createElement('script')
14 | let child = this.disqusCount
15 | let parent =
16 | document.getElementsByTagName('head')[0] ||
17 | document.getElementsByTagName('body')[0]
18 | child.async = true
19 | child.type = 'text/javascript'
20 | child.src = `https://${conf.shortname}.disqus.com/count.js`
21 | parent.appendChild(child)
22 | }
23 |
24 | removeDisqusScript() {
25 | if (this.disqusCount && this.disqusCount.parentNode) {
26 | this.disqusCount.parentNode.removeChild(this.disqusCount)
27 | this.disqusCount = null
28 | }
29 | }
30 |
31 | componentDidMount() {
32 | window.disqus_shortname = conf.shortname
33 | if (typeof window.DISQUSWIDGETS !== 'undefined') {
34 | window.DISQUSWIDGETS = undefined
35 | }
36 | this.addDisqusScript()
37 | }
38 |
39 | componentWillUnmount() {
40 | this.removeDisqusScript()
41 | }
42 |
43 | render() {
44 | return
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/modules/article/reducer.js:
--------------------------------------------------------------------------------
1 | import { REQUEST_ARTICLE, RECEIVE_ARTICLE } from './actionTypes'
2 | import { RECEIVE_CATEGORIES } from 'category/actionTypes'
3 |
4 | export default function article(state = initialState, action) {
5 | switch (action.type) {
6 | case REQUEST_ARTICLE:
7 | return {
8 | ...state,
9 | isFetching: {
10 | ...state.isFetching,
11 | [action.articleId]: true
12 | }
13 | }
14 |
15 | case RECEIVE_ARTICLE:
16 | return {
17 | ...state,
18 | isFetching: {
19 | ...state.isFetching,
20 | [action.articleId]: false
21 | },
22 | fetched: {
23 | ...state.fetched,
24 | [action.articleId]: true
25 | },
26 | texts: {
27 | ...state.texts,
28 | [action.articleId]: action.article
29 | }
30 | }
31 |
32 | case RECEIVE_CATEGORIES:
33 | return {
34 | ...state,
35 | articles: {
36 | ...state.articles,
37 | ...action.categories.articles
38 | }
39 | }
40 |
41 | default:
42 | return state
43 | }
44 | }
45 |
46 | const initialState = {
47 | isFetching: {},
48 | fetched: {},
49 | articles: {},
50 | texts: {}
51 | }
52 |
--------------------------------------------------------------------------------
/dev/middleware/resolveToUrl.js:
--------------------------------------------------------------------------------
1 | let fs = require('fs')
2 | let path = require('path')
3 | let dependencies = fs
4 | .readdirSync(path.join(process.cwd(), 'assets/js/vendors'))
5 | .map(file => file.slice(0, -3))
6 | let plugins = JSON.parse(
7 | fs.readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8')
8 | ).plugins
9 | let moduleResolver = plugins.find(
10 | plugin =>
11 | typeof plugin[0] !== 'undefined' && plugin[0] === 'module-resolver'
12 | )
13 | let aliases = moduleResolver[1]['alias']
14 |
15 | function resolver({ node: { source } }) {
16 | if (source !== null) {
17 | if (dependencies.includes(source.value)) {
18 | source.value = '/assets/js/vendors/' + source.value
19 | } else {
20 | if (
21 | !source.value.startsWith('/') &&
22 | !source.value.startsWith('./')
23 | ) {
24 | let alias = source.value.split('/')[0]
25 | if (typeof aliases[alias] !== 'undefined') {
26 | source.value =
27 | aliases[alias]['slice'](1) +
28 | source.value.slice(alias.length)
29 | } else {
30 | source.value = '/src/' + source.value
31 | }
32 | }
33 | }
34 | if (!source.value.endsWith('.js')) {
35 | source.value += '.js'
36 | }
37 | }
38 | }
39 |
40 | function resolveToUrl() {
41 | return {
42 | visitor: {
43 | ImportDeclaration: resolver
44 | }
45 | }
46 | }
47 |
48 | module.exports = resolveToUrl
49 |
--------------------------------------------------------------------------------
/src/components/disqus/disqusThread.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import conf from 'conf'
4 |
5 | export default class DisqusThread extends Component {
6 | constructor() {
7 | super()
8 | this.addDisqusScript = this.addDisqusScript.bind(this)
9 | this.removeDisqusScript = this.removeDisqusScript.bind(this)
10 | }
11 |
12 | addDisqusScript() {
13 | let child = (this.disqus = document.createElement('script'))
14 | let parent =
15 | document.getElementsByTagName('head')[0] ||
16 | document.getElementsByTagName('body')[0]
17 | child.async = true
18 | child.type = 'text/javascript'
19 | child.src = '//' + conf.shortname + '.disqus.com/embed.js'
20 | parent.appendChild(child)
21 | }
22 |
23 | removeDisqusScript() {
24 | if (this.disqus && this.disqus.parentNode) {
25 | this.disqus.parentNode.removeChild(this.disqus)
26 | this.disqus = null
27 | }
28 | }
29 |
30 | componentDidMount() {
31 | let { id, title } = this.props
32 | window.disqus_shortname = conf.shortname
33 | window.disqus_identifier = id
34 | window.disqus_title = title
35 | window.disqus_url = window.location.href
36 |
37 | if (typeof window.DISQUS !== 'undefined') {
38 | window.DISQUS.reset({ reload: true })
39 | } else {
40 | this.addDisqusScript()
41 | }
42 | }
43 |
44 | componentWillUnmount() {
45 | this.removeDisqusScript()
46 | }
47 |
48 | render() {
49 | return
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-without-webpack",
3 | "version": "2.0.0",
4 | "description": "React Without Webpack : The lightest React Starter Kit",
5 | "scripts": {
6 | "dev": "cross-env NODE_ENV=development node ./dev/dev-server.js",
7 | "local": "cross-env NODE_ENV=production node ./dev/dev-server.js",
8 | "build": "cross-env NODE_ENV=production node ./dev/build/build-app.js",
9 | "format": "./node_modules/.bin/prettier --write --no-semi --tab-width 4 --single-quote \"{,!(node_modules|assets)/**/}*.js\""
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/misterfresh/react-without-webpack"
14 | },
15 | "keywords": [
16 | "react",
17 | "import",
18 | "es-modules",
19 | "rollup"
20 | ],
21 | "author": "misterfresh",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/misterfresh/react-without-webpack"
25 | },
26 | "homepage": "https://github.com/misterfresh/react-without-webpack#readme",
27 | "dependencies": {
28 | "chokidar": "^2.0.4",
29 | "express": "^4.16.4",
30 | "lodash.debounce": "^4.0.8",
31 | "morgan": "^1.9.1"
32 | },
33 | "devDependencies": {
34 | "@babel/cli": "^7.2.3",
35 | "@babel/core": "^7.2.2",
36 | "@babel/plugin-proposal-class-properties": "^7.2.3",
37 | "@babel/plugin-syntax-object-rest-spread": "^7.2.0",
38 | "@babel/preset-react": "^7.0.0",
39 | "babel-plugin-module-resolver": "^3.1.1",
40 | "cross-env": "^5.2.0",
41 | "hasha": "^3.0.0",
42 | "prettier": "^1.15.3",
43 | "rollup": "^1.0.2",
44 | "rollup-plugin-babel": "^4.2.0",
45 | "rollup-plugin-node-resolve": "^4.0.0",
46 | "rollup-plugin-replace": "^2.1.0",
47 | "terser": "^3.14.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/assets/html/404_no_root.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/assets/html/404_with_root.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/blocks/category.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { pure } from 'recompose'
4 | import { Link } from 'react-router-dom'
5 |
6 | let Category = ({ category }) => (
7 |
13 |
14 |
19 | {category.title}
20 |
21 |
22 |
23 | )
24 |
25 | export default pure(Category)
26 |
27 | let styles = StyleSheet.create({
28 | category: {
29 | height: '30rem',
30 | backgroundPosition: 'center center',
31 | backgroundSize: 'cover',
32 | width: '100%',
33 | '@media (min-width: 768px)': {
34 | width: '100%'
35 | },
36 | '@media (min-width: 992px)': {
37 | width: '48%'
38 | },
39 | position: 'relative',
40 | marginBottom: '2rem',
41 | display: 'flex',
42 | alignItems: 'flex-end'
43 | },
44 | link: {
45 | color: '#fff',
46 | cursor: 'pointer',
47 | textDecoration: 'none',
48 | backgroundColor: 'transparent',
49 | fontSize: '2rem',
50 | borderBottom: 'solid 1px #fAfafa',
51 | ':hover': {
52 | cursor: 'pointer',
53 | textDecoration: 'none',
54 | backgroundColor: 'transparent',
55 | outline: 0,
56 | color: '#999',
57 | borderBottom: 'solid 1px #999',
58 | transition: 'all .4s'
59 | }
60 | },
61 | title: {
62 | background: 'rgba(50,50,50,.5)',
63 | width: '100%',
64 | fontSize: '2rem',
65 | color: '#fff',
66 | padding: '10px',
67 | fontWeight: 700,
68 | lineHeight: '1.1',
69 | bottom: 0,
70 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
71 | marginBottom: 0
72 | }
73 | })
74 |
--------------------------------------------------------------------------------
/src/components/form/baseInput.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import debounce from 'lodash.debounce'
3 |
4 | import uuid from 'utils/uuid'
5 | import capitalize from 'utils/capitalize'
6 |
7 | class BaseInput extends PureComponent {
8 | constructor(props) {
9 | super(props)
10 | this.handleChange = this.handleChange.bind(this)
11 | this.updateValue = debounce(this.updateValue.bind(this), 700)
12 | this.state = {
13 | value: props.value
14 | }
15 | this.id = uuid()
16 | }
17 |
18 | handleChange(event) {
19 | let property = event.target.dataset.property
20 | let value =
21 | this.props.type === 'number'
22 | ? parseInt(event.target.value)
23 | : event.target.value
24 | this.setState(
25 | {
26 | value
27 | },
28 | () => this.updateValue(property, value)
29 | )
30 | }
31 |
32 | componentWillReceiveProps(nextProps) {
33 | if (this.id !== document.activeElement.id) {
34 | this.setState({
35 | value: nextProps.value
36 | })
37 | }
38 | }
39 |
40 | updateValue(property, value) {
41 | this.setState(
42 | {
43 | value
44 | },
45 | () => this.props.onInput(property, value, this.props.id)
46 | )
47 | }
48 |
49 | render() {
50 | let {
51 | className = '',
52 | style = {},
53 | name = '',
54 | type = 'text',
55 | placeholder = '',
56 | Component = 'input',
57 | property = '',
58 | min = 0,
59 | step = 1
60 | } = this.props
61 | let { value } = this.state
62 | name = !!name ? name : property
63 | return (
64 |
77 | )
78 | }
79 | }
80 |
81 | export default BaseInput
82 |
--------------------------------------------------------------------------------
/dev/dev-server.js:
--------------------------------------------------------------------------------
1 | let express = require('express')
2 | let path = require('path')
3 | let fs = require('fs')
4 | let projectConf = require('./../conf')
5 | let projectRoot = projectConf.root ? `/${projectConf.root}/` : false
6 | let cachedConf = 'export default ' + JSON.stringify(projectConf)
7 |
8 | process.on('unhandledRejection', (reason, promise) => {
9 | if (reason.stack) {
10 | console.log(reason.stack)
11 | } else {
12 | console.log({ err: reason, promise: promise })
13 | }
14 | })
15 |
16 | const watcher = require('chokidar').watch([path.join(process.cwd(), 'src')])
17 | watcher.on('ready', function() {
18 | watcher.on('all', function() {
19 | Object.keys(require.cache).forEach(function(id) {
20 | if (/[\/\\]src[\/\\]/.test(id)) {
21 | delete require.cache[id]
22 | }
23 | })
24 | })
25 | })
26 |
27 | let index_dev = fs.readFileSync(
28 | path.join(process.cwd(), 'assets/html/index_dev.html'),
29 | 'utf-8'
30 | )
31 | if (projectRoot) {
32 | index_dev = index_dev
33 | .replace(/\/assets\//g, `${projectRoot}assets/`)
34 | .replace('/src/', `${projectRoot}src/`)
35 | }
36 | fs.writeFileSync(
37 | path.join(process.cwd(), 'assets/build/index_dev.html'),
38 | index_dev
39 | )
40 |
41 | fs.copyFileSync(
42 | path.join(
43 | process.cwd(),
44 | `assets/build/index_${
45 | process.env.NODE_ENV === 'production' ? 'prod' : 'dev'
46 | }.html`
47 | ),
48 | path.join(process.cwd(), 'index.html')
49 | )
50 |
51 | fs.copyFileSync(
52 | path.join(
53 | process.cwd(),
54 | `assets/html/404_${projectRoot ? 'with_root' : 'no_root'}.html`
55 | ),
56 | path.join(process.cwd(), '404.html')
57 | )
58 |
59 | const app = express()
60 | app.use(require('morgan')('dev'))
61 | app.use(function(req, res, next) {
62 | if (projectRoot && req.url.startsWith(projectRoot)) {
63 | req.url = req.url.slice(projectRoot.length - 1)
64 | }
65 | return next()
66 | })
67 | app.use(require('./middleware/update-middleware'))
68 | app.use(require('./middleware/transform-middleware'))
69 | app.get('/stream', function(req, res) {
70 | res.sseSetup()
71 | })
72 | app.get('/conf.js', function(req, res) {
73 | res.setHeader('Content-type', 'application/javascript')
74 | res.status(200).send(cachedConf)
75 | })
76 |
77 | app.use(express.static(process.cwd()))
78 | app.use(function(req, res, next) {
79 | res.status(404).sendFile(path.join(process.cwd(), '404.html'))
80 | })
81 | app.listen(8000, () =>
82 | console.log(
83 | `React drive cms listening on url: http://localhost:8000${projectRoot}`
84 | )
85 | )
86 |
--------------------------------------------------------------------------------
/assets/js/vendors/lodash.debounce.js:
--------------------------------------------------------------------------------
1 | function isObject(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function isObjectLike(t){return null!=t&&"object"==typeof t}function objectToString(t){return nativeObjectToString.call(t)}function getRawTag(t){var e=hasOwnProperty.call(t,symToStringTag),n=t[symToStringTag];try{t[symToStringTag]=void 0;var o=!0}catch(t){}var r=nativeObjectToString.call(t);return o&&(e?t[symToStringTag]=n:delete t[symToStringTag]),r}function baseGetTag(t){return null==t?void 0===t?undefinedTag:nullTag:symToStringTag&&symToStringTag in Object(t)?getRawTag(t):objectToString(t)}function isSymbol(t){return"symbol"==typeof t||isObjectLike(t)&&baseGetTag(t)==symbolTag}function toNumber(t){if("number"==typeof t)return t;if(isSymbol(t))return NAN;if(isObject(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=isObject(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(reTrim,"");var n=reIsBinary.test(t);return n||reIsOctal.test(t)?freeParseInt(t.slice(2),n?2:8):reIsBadHex.test(t)?NAN:+t}function debounce(t,e,n){function o(e){var n=f,o=b;return f=b=void 0,m=e,g=t.apply(o,n)}function r(t){return m=t,T=setTimeout(u,e),v?o(t):g}function i(t){var n=t-m,o=e-(t-y);return j?nativeMin(o,s-n):o}function a(t){var n=t-y,o=t-m;return void 0===y||n>=e||n<0||j&&o>=s}function u(){var t=now();if(a(t))return c(t);T=setTimeout(u,i(t))}function c(t){return T=void 0,S&&f?o(t):(f=b=void 0,g)}function l(){var t=now(),n=a(t);if(f=arguments,b=this,y=t,n){if(void 0===T)return r(y);if(j)return T=setTimeout(u,e),o(y)}return void 0===T&&(T=setTimeout(u,e)),g}var f,b,s,g,T,y,m=0,v=!1,j=!1,S=!0;if("function"!=typeof t)throw new TypeError(FUNC_ERROR_TEXT);return e=toNumber(e)||0,isObject(n)&&(v=!!n.leading,s=(j="maxWait"in n)?nativeMax(toNumber(n.maxWait)||0,e):s,S="trailing"in n?!!n.trailing:S),l.cancel=function(){void 0!==T&&clearTimeout(T),m=0,f=y=b=T=void 0},l.flush=function(){return void 0===T?g:c(now())},l}var objectProto=Object.prototype,nativeObjectToString=objectProto.toString,freeGlobal="object"==typeof global&&global&&global.Object===Object&&global,freeSelf="object"==typeof self&&self&&self.Object===Object&&self,root=freeGlobal||freeSelf||Function("return this")(),Symbol=root.Symbol,hasOwnProperty=(objectProto=Object.prototype).hasOwnProperty,nativeObjectToString=objectProto.toString,symToStringTag=Symbol?Symbol.toStringTag:void 0,now=function(){return root.Date.now()},nullTag="[object Null]",undefinedTag="[object Undefined]",symToStringTag=Symbol?Symbol.toStringTag:void 0,symbolTag="[object Symbol]",NAN=NaN,reTrim=/^\s+|\s+$/g,reIsBadHex=/^[-+]0x[0-9a-f]+$/i,reIsBinary=/^0b[01]+$/i,reIsOctal=/^0o[0-7]+$/i,freeParseInt=parseInt,FUNC_ERROR_TEXT="Expected a function",nativeMax=Math.max,nativeMin=Math.min;export default debounce;
--------------------------------------------------------------------------------
/src/modules/route/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { CALL_HISTORY_METHOD } from './actionTypes'
2 |
3 | import { getLocation, getQuery } from './selectors'
4 | import { getActiveEntityType } from 'entity/selectors'
5 | import { fetchEntityQueryIfNeeded } from 'entity/actionCreators'
6 | import serializeQuery from 'utils/serializeQuery'
7 |
8 | /**
9 | * This action type will be dispatched by the history actions below.
10 | * If you're writing a middleware to watch for navigation events, be sure to
11 | * look for actions of this type.
12 | */
13 |
14 | function updateLocation(method) {
15 | return (...args) => ({
16 | type: CALL_HISTORY_METHOD,
17 | location: { method, args }
18 | })
19 | }
20 |
21 | /**
22 | * These actions correspond to the history API.
23 | * The associated routerMiddleware will capture these events before they get to
24 | * your reducer and reissue them as the matching function on your history.
25 | */
26 | export const push = updateLocation('push')
27 | export const replace = updateLocation('replace')
28 | export const go = updateLocation('go')
29 | export const goBack = updateLocation('goBack')
30 | export const goForward = updateLocation('goForward')
31 |
32 | export const routerActions = { push, replace, go, goBack, goForward }
33 |
34 | export function updateQuery(property, value) {
35 | return (dispatch, getState) => {
36 | let state = getState()
37 | let query = getQuery(state)
38 | let location = getLocation(state)
39 | let entityType = getActiveEntityType(state)
40 | let queryValue = query[property]
41 | let label = ''
42 |
43 | if (!!value && typeof value === 'object') {
44 | label = value.label
45 | console.log('label is', label)
46 | value = value.value
47 | }
48 | if (!!value && (!queryValue || queryValue !== value)) {
49 | query = { ...query, [property]: value }
50 | } else {
51 | query = { ...query }
52 | delete query[property]
53 | }
54 |
55 | let requestQuery = serializeQuery(query)
56 | console.log({ reqqqqq: requestQuery, enttttt: entityType })
57 | return Promise.resolve(true)
58 | .then(chained =>
59 | dispatch(
60 | push({
61 | pathname: location.pathname,
62 | query,
63 | search: requestQuery,
64 | state: { ...location.state, label }
65 | })
66 | )
67 | )
68 | .then(updated =>
69 | dispatch(
70 | fetchEntityQueryIfNeeded(requestQuery, entityType, true)
71 | )
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/assets/css/fa.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'fontello';
3 | src: url('../font/fa.eot?76652049');
4 | src: url('../font/fa.eot?76652049#iefix') format('embedded-opentype'),
5 | url('../font/fa.woff2?76652049') format('woff2'),
6 | url('../font/fa.woff?76652049') format('woff'),
7 | url('../font/fa.ttf?76652049') format('truetype'),
8 | url('../font/fa.svg?76652049#fontello') format('svg');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
14 | /*
15 | @media screen and (-webkit-min-device-pixel-ratio:0) {
16 | @font-face {
17 | font-family: 'fontello';
18 | src: url('../font/fontello.svg?76652049#fontello') format('svg');
19 | }
20 | }
21 | */
22 |
23 | [class^="icon-"]:before, [class*=" icon-"]:before {
24 | font-family: "fontello";
25 | font-style: normal;
26 | font-weight: normal;
27 | speak: none;
28 |
29 | display: inline-block;
30 | text-decoration: inherit;
31 | width: 1em;
32 | margin-right: .2em;
33 | text-align: center;
34 | /* opacity: .8; */
35 |
36 | /* For safety - reset parent styles, that can break glyph codes*/
37 | font-variant: normal;
38 | text-transform: none;
39 |
40 | /* fix buttons height, for twitter bootstrap */
41 | line-height: 1em;
42 |
43 | /* Animation center compensation - margins should be symmetric */
44 | /* remove if not needed */
45 | margin-left: .2em;
46 |
47 | /* you can be more comfortable with increased icons size */
48 | /* font-size: 120%; */
49 |
50 | /* Font smoothing. That was taken from TWBS */
51 | -webkit-font-smoothing: antialiased;
52 | -moz-osx-font-smoothing: grayscale;
53 |
54 | /* Uncomment for 3D effect */
55 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
56 | }
57 |
58 | .icon-bookmark:before { content: '\e800'; } /* '' */
59 | .icon-user:before { content: '\e801'; } /* '' */
60 | .icon-home:before { content: '\e802'; } /* '' */
61 | .icon-right-open:before { content: '\e803'; } /* '' */
62 | .icon-left-open:before { content: '\e804'; } /* '' */
63 | .icon-up-open:before { content: '\e805'; } /* '' */
64 | .icon-down-open:before { content: '\e806'; } /* '' */
65 | .icon-star:before { content: '\e807'; } /* '' */
66 | .icon-heart:before { content: '\e808'; } /* '' */
67 | .icon-link:before { content: '\e809'; } /* '' */
68 | .icon-picture:before { content: '\e80a'; } /* '' */
69 | .icon-phone:before { content: '\e80b'; } /* '' */
70 | .icon-twitter:before { content: '\f099'; } /* '' */
71 | .icon-mail-alt:before { content: '\f0e0'; } /* '' */
72 | .icon-paper-plane:before { content: '\f1d8'; } /* '' */
73 | .icon-facebook-official:before { content: '\f230'; } /* '' */
74 | .icon-facebook-squared:before { content: '\f308'; } /* '' */
75 |
--------------------------------------------------------------------------------
/assets/html/index_prod.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | React Drive CMS
9 |
10 |
11 |
12 |
34 |
35 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/assets/html/index_dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | React Drive CMS
9 |
10 |
11 |
12 |
34 |
35 |
36 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/blocks/article.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { pure } from 'recompose'
4 | import { Link } from 'react-router-dom'
5 |
6 | let Article = ({ article, category }) => (
7 |
8 |
9 |
14 | {article.title}
15 |
16 |
17 | {article.subtitle}
18 |
19 |
29 | - Published in :
30 |
35 | {category.title}
36 |
37 |
38 |
39 | )
40 |
41 | export default pure(Article)
42 |
43 | let styles = StyleSheet.create({
44 | article: {
45 | padding: '30px 0',
46 | display: 'block',
47 | borderBottom: 'solid 1px #f5f5f5'
48 | },
49 | title: {
50 | textDecoration: 'none',
51 | color: '#333337',
52 | fontSize: '2.4rem',
53 | marginTop: 0,
54 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
55 | fontWeight: 700,
56 | marginBottom: '10px',
57 | lineHeight: '1.1',
58 | cursor: 'pointer',
59 | backgroundColor: 'transparent',
60 | border: 'none',
61 | '@media (min-width: 992px)': { fontSize: '3.2rem' },
62 | ':hover': { color: '#b6b6b6' }
63 | },
64 | p: {
65 | margin: '0 0 10px',
66 | fontFamily: '"Droid Serif",serif',
67 | fontSize: '1.6rem',
68 | '@media (min-width: 992px)': {
69 | fontSize: '1.8rem'
70 | }
71 | },
72 | description: {
73 | marginBottom: '30px'
74 | },
75 | meta: {
76 | color: '#b6b6b6'
77 | },
78 | comments: {},
79 | category: {
80 | textDecoration: 'none',
81 | cursor: 'pointer',
82 | backgroundColor: 'transparent',
83 | outline: 0,
84 | transition: 'all .4s',
85 | color: '#b6b6b6',
86 | borderBottom: '1px solid #b6b6b6',
87 | ':hover': {
88 | textDecoration: 'none',
89 | cursor: 'pointer',
90 | backgroundColor: 'transparent',
91 | color: '#333337',
92 | outline: 0,
93 | transition: 'all .4s',
94 | borderBottom: '1px solid #b6b6b6'
95 | }
96 | }
97 | })
98 |
--------------------------------------------------------------------------------
/assets/build/index_prod.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | React Drive CMS
9 |
10 |
11 |
12 |
34 |
35 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | React Drive CMS
9 |
10 |
11 |
12 |
34 |
35 |
36 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/assets/build/index_dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | React Drive CMS
9 |
10 |
11 |
12 |
34 |
35 |
36 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/dev/middleware/update-middleware.js:
--------------------------------------------------------------------------------
1 | let chokidar = require('chokidar')
2 | let path = require('path')
3 | let fs = require('fs')
4 | let crypto = require('crypto')
5 | let serverStart = Date.now()
6 | let update = require('./update')
7 | let debounce = require('lodash.debounce')
8 | let cache = require('./cache')
9 |
10 | let refresh = debounce(function(res) {
11 | Object.keys(cache.getAll('stale')).forEach(file => {
12 | let src = file.replace(/\\/gi, '/')
13 | let fileUri = file.split(process.cwd())[1].replace(/\\/gi, '/')
14 | fs.lstat(file, function(err, stats) {
15 | if (err) {
16 | res.status(500).json(err)
17 | } else {
18 | let mtime = stats.mtime.getTime()
19 | let lastModifiedHash = crypto
20 | .createHash('md5')
21 | .update(mtime + '-' + src)
22 | .digest('hex')
23 |
24 | let lastKnownHash = cache.get('hashMap', src)
25 | if (!lastKnownHash && lastKnownHash === lastModifiedHash) {
26 | res.write('data: ' + fileUri + '\n\n')
27 | cache.remove('stale', file)
28 | } else {
29 | update(file, lastModifiedHash, function(err, updated) {
30 | if (err) {
31 | res.status(500).json(err)
32 | } else {
33 | res.write('data: ' + fileUri + '\n\n')
34 | cache.remove('stale', file)
35 | }
36 | })
37 | }
38 | }
39 | })
40 | })
41 | })
42 |
43 | function updater(req, res, next) {
44 | if (typeof res.sseSetup !== 'undefined') {
45 | return next()
46 | }
47 |
48 | res.sseSetup = function() {
49 | res.writeHead(200, {
50 | 'Content-Type': 'text/event-stream',
51 | 'Cache-Control': 'no-cache',
52 | Connection: 'keep-alive'
53 | })
54 | serverStart = Date.now()
55 | chokidar
56 | .watch(path.join(process.cwd(), 'src'), {
57 | ignored: /(^|[\/\\])\../
58 | })
59 | .on('all', (event, filePath) => {
60 | if (
61 | Date.now() - serverStart > 10000 &&
62 | !(
63 | filePath.endsWith('___jb_tmp___') ||
64 | filePath.endsWith('___jb_old___')
65 | )
66 | ) {
67 | if (event === 'change' || event === 'add') {
68 | console.log(event, filePath)
69 | cache.set('stale', filePath)
70 | refresh(res)
71 | } else if (event === 'unlink') {
72 | console.log('unlink', filePath)
73 | cache.remove('stale', filePath)
74 | let link =
75 | '/src/' +
76 | filePath.replace(/\\/gi, '/').split('/src/')[1]
77 | cache.remove('links', link)
78 | }
79 | }
80 | })
81 | }
82 |
83 | return next()
84 | }
85 |
86 | module.exports = updater
87 |
--------------------------------------------------------------------------------
/src/routes/about.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { Link } from 'react-router-dom'
4 | import Page from 'components/layout/page'
5 | import conf from 'conf'
6 |
7 | let About = () => (
8 |
19 |
20 |
28 |
29 |
React Drive CMS Demo
30 |
31 | A demo site to showcase the use of Google Drive as a Content
32 | Management System. Write articles in Google Docs and publish
33 | them directly from there.
34 |
35 |
36 | Google Drive is the backend, only a few static files are
37 | hosted on GitHub Pages, and the content is displayed with
38 | React JS.
39 |
40 |
41 |
42 |
43 |
44 |
45 | Contact
46 |
47 |
48 |
49 | )
50 |
51 | export default About
52 |
53 | let styles = StyleSheet.create({
54 | content: { display: 'block' },
55 | image: {
56 | borderRadius: '50%',
57 | width: '150px',
58 | border: 0,
59 | maxWidth: '100%',
60 | verticalAlign: 'middle',
61 | float: 'left',
62 | marginRight: '2rem'
63 | },
64 | info: {},
65 | title: {
66 | margin: '30px 0 20px',
67 | fontSize: '3.8rem',
68 | fontWeight: 700,
69 | lineHeight: '1.1',
70 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif'
71 | },
72 | p: {
73 | fontSize: '2rem',
74 | margin: '0 0 10px',
75 | marginBottom: '30px'
76 | },
77 | footer: {
78 | padding: '10px 0',
79 | fontSize: '1.4rem',
80 | letterSpacing: '1px',
81 | fontWeight: 700,
82 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
83 | textTransform: 'uppercase'
84 | },
85 | contact: {
86 | textDecoration: 'none',
87 | backgroundColor: 'transparent',
88 | color: '#999',
89 | borderBottom: 'none',
90 | fontSize: '1.4rem',
91 | ':hover': {
92 | textDecoration: 'none',
93 | backgroundColor: 'transparent',
94 | color: '#333',
95 | outline: 0,
96 | transition: 'all .4s',
97 | borderBottom: 'none'
98 | }
99 | }
100 | })
101 |
--------------------------------------------------------------------------------
/dev/build/build-app.js:
--------------------------------------------------------------------------------
1 | let rollup = require('rollup')
2 | let resolve = require('rollup-plugin-node-resolve')
3 | let replace = require('rollup-plugin-replace')
4 | let babel = require('rollup-plugin-babel')
5 | let fs = require('fs')
6 | let path = require('path')
7 | let Terser = require('terser')
8 | let hasha = require('hasha')
9 | let projectConf = require('./../../conf')
10 | let projectRoot = projectConf.root ? `/${projectConf.root}/` : false
11 | fs.writeFileSync(
12 | path.join(process.cwd(), 'assets/build/conf.js'),
13 | 'export default ' + JSON.stringify(projectConf)
14 | )
15 |
16 | let dependencies = {}
17 | fs.readdirSync(path.join(process.cwd(), 'assets/js/vendors')).forEach(file => {
18 | dependencies[file.slice(0, -3)] = `./assets/js/vendors/${file}`
19 | })
20 | let conf = JSON.parse(
21 | fs.readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8')
22 | )
23 | let moduleResolverIndex = conf.plugins.findIndex(
24 | plugin =>
25 | typeof plugin[0] !== 'undefined' && plugin[0] === 'module-resolver'
26 | )
27 | conf.babelrc = false
28 | conf.exclude = 'node_modules/**'
29 | conf.plugins[moduleResolverIndex][1]['alias'] = {
30 | ...conf.plugins[moduleResolverIndex][1]['alias'],
31 | ...dependencies,
32 | conf: './assets/build/conf.js'
33 | }
34 |
35 | let index_prod = fs.readFileSync(
36 | path.join(process.cwd(), 'assets/html/index_prod.html'),
37 | 'utf-8'
38 | )
39 | if (projectRoot) {
40 | index_prod = index_prod
41 | .replace(/\/assets\//g, `${projectRoot}assets/`)
42 | .replace('/src/', `${projectRoot}src/`)
43 | }
44 | fs.copyFileSync(
45 | path.join(
46 | process.cwd(),
47 | `assets/html/404_${projectRoot ? 'with_root' : 'no_root'}.html`
48 | ),
49 | path.join(process.cwd(), '404.html')
50 | )
51 |
52 | rollup
53 | .rollup({
54 | input: path.join(process.cwd(), 'src/app.js'),
55 | plugins: [
56 | replace({
57 | 'process.env.NODE_ENV': `"production"`
58 | }),
59 | resolve({
60 | module: true,
61 | jsnext: true,
62 | extensions: ['.js'],
63 | browser: true
64 | }),
65 | babel(conf)
66 | ]
67 | })
68 | .then(bundle => {
69 | bundle
70 | .generate({
71 | format: 'iife',
72 | moduleName: 'ReactDriveCMS'
73 | })
74 | .then(result => {
75 | let { code } = result.output[0]
76 | let hash = hasha(code, { algorithm: 'sha1' }).slice(0, 16)
77 | fs.writeFile(
78 | path.join(process.cwd(), 'assets/build/index_prod.html'),
79 | index_prod.replace('', hash),
80 | (err, res) => {
81 | if (err) {
82 | console.log(err)
83 | } else {
84 | fs.copyFile(
85 | path.join(
86 | process.cwd(),
87 | 'assets/build/index_prod.html'
88 | ),
89 | path.join(process.cwd(), 'index.html'),
90 | (err, res) => {
91 | if (err) {
92 | console.log(err)
93 | }
94 | }
95 | )
96 | }
97 | }
98 | )
99 |
100 | code = Terser.minify(code).code
101 | return fs.writeFile(
102 | path.join(process.cwd(), `assets/build/app.min.${hash}.js`),
103 | code,
104 | (err, res) => {
105 | if (err) {
106 | console.log(err)
107 | } else {
108 | console.log('Build complete.')
109 | }
110 | }
111 | )
112 | })
113 | })
114 | .catch(error => console.log(error))
115 |
--------------------------------------------------------------------------------
/src/components/layout/sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { pure } from 'recompose'
3 | import { StyleSheet, css } from 'aphrodite'
4 | import { Link } from 'react-router-dom'
5 |
6 | let Sidebar = ({
7 | title,
8 | subtitle,
9 | description,
10 | sidebarImage,
11 | menuVisible,
12 | showLinks
13 | }) => (
14 |
18 |
19 |
20 |
{title}
21 |
{subtitle}
22 |
{description}
23 |
24 |
40 |
41 |
42 | )
43 |
44 | export default pure(Sidebar)
45 |
46 | let styles = StyleSheet.create({
47 | sidebar: {
48 | justifyContent: 'flex-start',
49 | alignItems: 'flex-end',
50 | transition:
51 | 'width linear 750ms, height linear 750ms, left linear 750ms',
52 | padding: 0,
53 | backgroundPosition: 'center',
54 | backgroundRepeat: 'no-repeat',
55 | backgroundSize: 'cover',
56 | display: 'flex',
57 | position: 'relative',
58 | width: '100%',
59 | '@media (min-width: 768px)': {
60 | height: '45rem',
61 | position: 'relative',
62 | width: '100%'
63 | },
64 | '@media (min-width: 992px)': {
65 | height: '100vh',
66 | backgroundColor: '#f5f5f5',
67 | position: 'fixed',
68 | width: '40%',
69 | left: 0
70 | },
71 | '@media (min-width: 1200px)': {
72 | height: '100vh',
73 | backgroundColor: '#f5f5f5',
74 | position: 'fixed',
75 | width: '40%',
76 | left: 0
77 | },
78 | overflowX: 'hidden',
79 | maxWidth: '100%'
80 | },
81 | sidebarNarrow: {
82 | padding: 0,
83 | backgroundPosition: 'center',
84 | backgroundRepeat: 'no-repeat',
85 | backgroundSize: 'cover',
86 | display: 'flex',
87 | width: '100%',
88 | '@media (min-width: 768px)': {
89 | height: '45rem',
90 | position: 'relative',
91 | width: '100%'
92 | },
93 | '@media (min-width: 992px)': {
94 | height: '45rem',
95 | position: 'relative',
96 | width: '100%'
97 | },
98 | '@media (min-width: 1200px)': {
99 | height: '100vh',
100 | backgroundColor: '#f5f5f5',
101 | position: 'fixed',
102 | width: '35%',
103 | left: '25%'
104 | }
105 | },
106 | info: {
107 | padding: '5%',
108 | background: 'rgba(50,50,50,.5)',
109 | color: '#fafafa',
110 | height: '28rem',
111 | width: '100%',
112 | display: 'flex',
113 | justifyContent: 'flex-end',
114 | alignItems: 'end',
115 | flexDirection: 'column'
116 | },
117 |
118 | primary: {
119 | borderBottom: 'solid 1px rgba(255,255,255,.3)',
120 | marginBottom: '1.6rem'
121 | },
122 | h1: {
123 | letterSpacing: 0,
124 | marginBottom: 0,
125 | fontSize: '3.4rem',
126 | textShadow: '0 1px 3px rgba(0,0,0,.3)',
127 | fontWeight: 700,
128 | fontFamily: "'Source Sans Pro',Helvetica,Arial,sans-serif"
129 | },
130 | p: {
131 | marginBottom: 10,
132 | textShadow: '0 1px 3px rgba(0,0,0,.3)',
133 | lineHeight: '2.4rem',
134 | fontSize: '1.8rem'
135 | },
136 | links: {
137 | display: 'none'
138 | },
139 | showLinks: {
140 | display: 'flex'
141 | },
142 | button: {
143 | fontFamily: "'Source Sans Pro',Helvetica,Arial,sans-serif",
144 | display: 'inline-block',
145 | color: '#fff',
146 | marginRight: '20px',
147 | marginBottom: 0,
148 | backgroundColor: '#337ab7',
149 | borderColor: '#2e6da4',
150 | fontWeight: 400,
151 | textAlign: 'center',
152 | touchAction: 'manipulation',
153 | cursor: 'pointer',
154 | border: '1px solid transparent',
155 | whiteSpace: 'nowrap',
156 | padding: '6px 12px',
157 | fontSize: '14px',
158 | lineHeight: '1.42857',
159 | borderRadius: '4px',
160 | userSelect: 'none',
161 | ':hover': {
162 | color: '#fff',
163 | backgroundColor: '#286090',
164 | borderColor: '#204d74',
165 | textDecoration: 'none'
166 | },
167 | ':focus': {
168 | color: '#fff',
169 | backgroundColor: '#286090',
170 | borderColor: '#122b40',
171 | textDecoration: 'none'
172 | }
173 | }
174 | })
175 |
--------------------------------------------------------------------------------
/src/routes/home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import { Link } from 'react-router-dom'
6 |
7 | import { getLocation } from 'route/selectors'
8 |
9 | import Page from 'components/layout/page'
10 | import DisqusCount from 'components/disqus/disqusCount'
11 | import Article from 'components/blocks/article'
12 | import Category from 'components/blocks/category'
13 | import blocks from 'styles/blocks'
14 | import conf from 'conf'
15 |
16 | class Home extends Component {
17 | constructor() {
18 | super()
19 | this.setActivePanel = this.setActivePanel.bind(this)
20 | this.state = {
21 | activePanel: 'posts'
22 | }
23 | }
24 |
25 | setActivePanel() {
26 | let panel = event.target.dataset.panel
27 | this.setState({
28 | activePanel: panel
29 | })
30 | }
31 |
32 | render() {
33 | let { location, categories, articles } = this.props
34 | let { activePanel } = this.state
35 | return (
36 |
47 |
48 |
56 | Posts
57 |
58 |
66 | Categories
67 |
68 |
69 |
75 | {Object.values(articles).map(article => (
76 |
81 | ))}
82 |
83 |
91 | {Object.values(categories).map(category => (
92 |
93 | ))}
94 |
95 |
96 |
97 | )
98 | }
99 | }
100 |
101 | function mapStateToProps(state) {
102 | return {
103 | location: getLocation(state),
104 | categories: state.category.categories,
105 | articles: state.article.articles
106 | }
107 | }
108 |
109 | function mapDispatchToProps(dispatch) {
110 | return Object.assign(bindActionCreators({}, dispatch), { dispatch })
111 | }
112 |
113 | export default connect(
114 | mapStateToProps,
115 | mapDispatchToProps
116 | )(Home)
117 |
118 | let styles = StyleSheet.create({
119 | subNav: {
120 | borderBottom: 'solid 1px #f5f5f5',
121 | lineHeight: '3rem'
122 | },
123 | button: {
124 | borderBottom: 'solid 2px #000',
125 | display: 'inline-block',
126 | fontWeight: 700,
127 | marginRight: '10px',
128 | fontSize: '1.2rem',
129 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
130 | textTransform: 'uppercase',
131 | cursor: 'pointer',
132 | backgroundColor: 'transparent',
133 | outline: 0,
134 | lineHeight: '30px',
135 | letterSpacing: '2pt',
136 | textDecoration: 'none',
137 | color: '#b6b6b6',
138 | border: 'none',
139 | ':active': {
140 | borderBottom: 'solid 2px #000',
141 | color: '#333337'
142 | },
143 | ':hover': {
144 | borderBottom: 'solid 2px #000',
145 | color: '#333337'
146 | },
147 | ':focus': {
148 | outline: 0
149 | }
150 | },
151 | buttonActive: {
152 | borderBottom: 'solid 2px #000',
153 | color: '#333337'
154 | },
155 | list: {
156 | animation: 'fadein 2s',
157 | display: 'flex',
158 | flexWrap: 'wrap',
159 | width: '100%',
160 | justifyContent: 'space-between'
161 | },
162 |
163 | hide: {
164 | position: 'absolute',
165 | top: '-9999px',
166 | left: '-9999px',
167 | display: 'none'
168 | }
169 | })
170 |
--------------------------------------------------------------------------------
/src/routes/category.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import { Link } from 'react-router-dom'
6 |
7 | import { getLocation } from 'route/selectors'
8 |
9 | import Page from 'components/layout/page'
10 | import DisqusCount from 'components/disqus/disqusCount'
11 | import Article from 'components/blocks/article'
12 | import Category from 'components/blocks/category'
13 |
14 | class CategoryPage extends Component {
15 | constructor() {
16 | super()
17 | this.setActivePanel = this.setActivePanel.bind(this)
18 | this.state = {
19 | activePanel: 'posts'
20 | }
21 | }
22 |
23 | setActivePanel() {
24 | let panel = event.target.dataset.panel
25 | this.setState({
26 | activePanel: panel
27 | })
28 | }
29 |
30 | componentWillReceiveProps(nextProps) {
31 | let { match } = this.props
32 | let activeCategoryId = match.params.categoryId
33 | let nextCategoryId = nextProps.match.params.categoryId
34 | if (activeCategoryId !== nextCategoryId) {
35 | this.setState({
36 | activePanel: 'posts'
37 | })
38 | }
39 | }
40 |
41 | render() {
42 | let { location, categories, articles, match } = this.props
43 | let activeCategoryId = match.params.categoryId
44 | let activeCategory = categories[activeCategoryId]
45 | ? categories[activeCategoryId]
46 | : {
47 | title: '',
48 | image: `${window.location.protocol}//${
49 | window.location.hostname
50 | }:${window.location.port}/assets/images/default-sidebar.jpg`
51 | }
52 |
53 | let { activePanel } = this.state
54 | return (
55 |
61 |
62 |
70 | Posts
71 |
72 |
80 | Categories
81 |
82 |
83 |
84 | {activePanel === 'posts' &&
85 | Object.values(articles)
86 | .filter(
87 | article =>
88 | article.categoryId === activeCategoryId
89 | )
90 | .map(article => (
91 |
96 | ))}
97 | {activePanel === 'categories' &&
98 | Object.values(categories).map(category => (
99 |
100 | ))}
101 |
102 |
103 |
104 | )
105 | }
106 | }
107 |
108 | function mapStateToProps(state) {
109 | return {
110 | location: getLocation(state),
111 | categories: state.category.categories,
112 | articles: state.article.articles
113 | }
114 | }
115 |
116 | function mapDispatchToProps(dispatch) {
117 | return Object.assign(bindActionCreators({}, dispatch), { dispatch })
118 | }
119 |
120 | export default connect(
121 | mapStateToProps,
122 | mapDispatchToProps
123 | )(CategoryPage)
124 |
125 | let styles = StyleSheet.create({
126 | subNav: {
127 | borderBottom: 'solid 1px #f5f5f5',
128 | lineHeight: '3rem'
129 | },
130 | button: {
131 | borderBottom: 'solid 2px #000',
132 | display: 'inline-block',
133 | fontWeight: 700,
134 | marginRight: '10px',
135 | fontSize: '1.2rem',
136 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
137 | textTransform: 'uppercase',
138 | cursor: 'pointer',
139 | backgroundColor: 'transparent',
140 | outline: 0,
141 | lineHeight: '30px',
142 | letterSpacing: '2pt',
143 | textDecoration: 'none',
144 | color: '#b6b6b6',
145 | border: 'none',
146 | ':active': {
147 | borderBottom: 'solid 2px #000',
148 | color: '#333337'
149 | },
150 | ':hover': {
151 | borderBottom: 'solid 2px #000',
152 | color: '#333337'
153 | },
154 | ':focus': {
155 | outline: 0
156 | }
157 | },
158 | buttonActive: {
159 | borderBottom: 'solid 2px #000',
160 | color: '#333337'
161 | },
162 | list: {
163 | animation: 'fadein 2s',
164 | display: 'flex',
165 | flexWrap: 'wrap',
166 | width: '100%',
167 | justifyContent: 'space-between'
168 | }
169 | })
170 |
--------------------------------------------------------------------------------
/assets/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: sans-serif;
9 | line-height: 1.15;
10 | -webkit-text-size-adjust: 100%;
11 | -ms-text-size-adjust: 100%;
12 | -ms-overflow-style: scrollbar;
13 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
14 | }
15 |
16 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
17 | display: block;
18 | }
19 |
20 | body {
21 | margin: 0;
22 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
23 | font-size: 1rem;
24 | font-weight: 400;
25 | line-height: 1.5;
26 | color: #212529;
27 | text-align: left;
28 | background-color: #fff;
29 | }
30 |
31 | [tabindex="-1"]:focus {
32 | outline: 0 !important;
33 | }
34 |
35 | hr {
36 | box-sizing: content-box;
37 | height: 0;
38 | overflow: visible;
39 | }
40 |
41 | h1, h2, h3, h4, h5, h6 {
42 | margin-top: 0;
43 | margin-bottom: 0.5rem;
44 | }
45 |
46 | p {
47 | margin-top: 0;
48 | margin-bottom: 1rem;
49 | }
50 |
51 | abbr[title],
52 | abbr[data-original-title] {
53 | text-decoration: underline;
54 | -webkit-text-decoration: underline dotted;
55 | text-decoration: underline dotted;
56 | cursor: help;
57 | border-bottom: 0;
58 | }
59 |
60 | address {
61 | margin-bottom: 1rem;
62 | font-style: normal;
63 | line-height: inherit;
64 | }
65 |
66 | ol,
67 | ul,
68 | dl {
69 | margin-top: 0;
70 | margin-bottom: 1rem;
71 | }
72 |
73 | ol ol,
74 | ul ul,
75 | ol ul,
76 | ul ol {
77 | margin-bottom: 0;
78 | }
79 |
80 | dt {
81 | font-weight: 700;
82 | }
83 |
84 | dd {
85 | margin-bottom: .5rem;
86 | margin-left: 0;
87 | }
88 |
89 | blockquote {
90 | margin: 0 0 1rem;
91 | }
92 |
93 | dfn {
94 | font-style: italic;
95 | }
96 |
97 | b,
98 | strong {
99 | font-weight: bolder;
100 | }
101 |
102 | small {
103 | font-size: 80%;
104 | }
105 |
106 | sub,
107 | sup {
108 | position: relative;
109 | font-size: 75%;
110 | line-height: 0;
111 | vertical-align: baseline;
112 | }
113 |
114 | sub {
115 | bottom: -.25em;
116 | }
117 |
118 | sup {
119 | top: -.5em;
120 | }
121 |
122 | a {
123 | color: #007bff;
124 | text-decoration: none;
125 | background-color: transparent;
126 | -webkit-text-decoration-skip: objects;
127 | }
128 |
129 | a:hover {
130 | color: #0056b3;
131 | text-decoration: underline;
132 | }
133 |
134 | a:not([href]):not([tabindex]) {
135 | color: inherit;
136 | text-decoration: none;
137 | }
138 |
139 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
140 | color: inherit;
141 | text-decoration: none;
142 | }
143 |
144 | a:not([href]):not([tabindex]):focus {
145 | outline: 0;
146 | }
147 |
148 | pre,
149 | code,
150 | kbd,
151 | samp {
152 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
153 | font-size: 1em;
154 | }
155 |
156 | pre {
157 | margin-top: 0;
158 | margin-bottom: 1rem;
159 | overflow: auto;
160 | -ms-overflow-style: scrollbar;
161 | }
162 |
163 | figure {
164 | margin: 0 0 1rem;
165 | }
166 |
167 | img {
168 | vertical-align: middle;
169 | border-style: none;
170 | }
171 |
172 | svg {
173 | overflow: hidden;
174 | vertical-align: middle;
175 | }
176 |
177 | table {
178 | border-collapse: collapse;
179 | }
180 |
181 | caption {
182 | padding-top: 0.75rem;
183 | padding-bottom: 0.75rem;
184 | color: #6c757d;
185 | text-align: left;
186 | caption-side: bottom;
187 | }
188 |
189 | th {
190 | text-align: inherit;
191 | }
192 |
193 | label {
194 | display: inline-block;
195 | margin-bottom: 0.5rem;
196 | }
197 |
198 | button {
199 | border-radius: 0;
200 | }
201 |
202 | button:focus {
203 | outline: 1px dotted;
204 | outline: 5px auto -webkit-focus-ring-color;
205 | }
206 |
207 | input,
208 | button,
209 | select,
210 | optgroup,
211 | textarea {
212 | margin: 0;
213 | font-family: inherit;
214 | font-size: inherit;
215 | line-height: inherit;
216 | }
217 |
218 | button,
219 | input {
220 | overflow: visible;
221 | }
222 |
223 | button,
224 | select {
225 | text-transform: none;
226 | }
227 |
228 | button,
229 | html [type="button"],
230 | [type="reset"],
231 | [type="submit"] {
232 | -webkit-appearance: button;
233 | }
234 |
235 | button::-moz-focus-inner,
236 | [type="button"]::-moz-focus-inner,
237 | [type="reset"]::-moz-focus-inner,
238 | [type="submit"]::-moz-focus-inner {
239 | padding: 0;
240 | border-style: none;
241 | }
242 |
243 | input[type="radio"],
244 | input[type="checkbox"] {
245 | box-sizing: border-box;
246 | padding: 0;
247 | }
248 |
249 | input[type="date"],
250 | input[type="time"],
251 | input[type="datetime-local"],
252 | input[type="month"] {
253 | -webkit-appearance: listbox;
254 | }
255 |
256 | textarea {
257 | overflow: auto;
258 | resize: vertical;
259 | }
260 |
261 | fieldset {
262 | min-width: 0;
263 | padding: 0;
264 | margin: 0;
265 | border: 0;
266 | }
267 |
268 | legend {
269 | display: block;
270 | width: 100%;
271 | max-width: 100%;
272 | padding: 0;
273 | margin-bottom: .5rem;
274 | font-size: 1.5rem;
275 | line-height: inherit;
276 | color: inherit;
277 | white-space: normal;
278 | }
279 |
280 | progress {
281 | vertical-align: baseline;
282 | }
283 |
284 | [type="number"]::-webkit-inner-spin-button,
285 | [type="number"]::-webkit-outer-spin-button {
286 | height: auto;
287 | }
288 |
289 | [type="search"] {
290 | outline-offset: -2px;
291 | -webkit-appearance: none;
292 | }
293 |
294 | [type="search"]::-webkit-search-cancel-button,
295 | [type="search"]::-webkit-search-decoration {
296 | -webkit-appearance: none;
297 | }
298 |
299 | ::-webkit-file-upload-button {
300 | font: inherit;
301 | -webkit-appearance: button;
302 | }
303 |
304 | output {
305 | display: inline-block;
306 | }
307 |
308 | summary {
309 | display: list-item;
310 | cursor: pointer;
311 | }
312 |
313 | template {
314 | display: none;
315 | }
316 |
317 | [hidden] {
318 | display: none !important;
319 | }
320 |
--------------------------------------------------------------------------------
/assets/font/fa.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Copyright (C) 2019 by original authors @ fontello.com
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/lib/drive.js:
--------------------------------------------------------------------------------
1 | import Api from './api'
2 | import conf from 'conf'
3 |
4 | class Drive extends Api {
5 | constructor() {
6 | super()
7 | this.getSpreadsheet = this.getSpreadsheet.bind(this)
8 | this.getDocument = this.getDocument.bind(this)
9 | this.getCategories = this.getCategories.bind(this)
10 | this.getArticleHtml = this.getArticleHtml.bind(this)
11 | this.driveExportUrl = 'https://drive.google.com/uc?export=download&id='
12 | this.slug = this.slug.bind(this)
13 | this.formatDate = this.formatDate.bind(this)
14 | }
15 |
16 | getSpreadsheet(fileId) {
17 | return this.get(
18 | `https://spreadsheets.google.com/feeds/list/${fileId}/od6/public/values?alt=json`,
19 | {
20 | credentials: 'omit'
21 | }
22 | )
23 | .then(response => response.json())
24 | .catch(error => console.log('error', error))
25 | }
26 |
27 | getDocument(fileId) {
28 | return this.get(
29 | `https://docs.google.com/feeds/download/documents/export/Export?id=${fileId}&exportFormat=html`,
30 | {
31 | credentials: 'omit'
32 | }
33 | )
34 | .then(response => {
35 | return response.text()
36 | })
37 | .catch(error => console.log('error', error))
38 | }
39 |
40 | getCategories() {
41 | return this.getSpreadsheet(conf.dashboardId).then(sheetData => {
42 | let articles = {}
43 | let categories = {}
44 |
45 | sheetData.feed.entry
46 | .map(row => ({
47 | title: row.gsx$title.$t,
48 | subtitle: row.gsx$subtitle.$t,
49 | image: row.gsx$image.$t,
50 | category: row.gsx$category.$t,
51 | postId: row.gsx$postid.$t,
52 | imageId: row.gsx$imageid.$t,
53 | lastUpdated: row.gsx$lastupdated.$t
54 | }))
55 | .forEach(row => {
56 | let category = {}
57 |
58 | let categoryId = this.slug(row.category, 'category')
59 |
60 | let existingCategory = Object.values(categories).find(
61 | category => category.id === categoryId
62 | )
63 |
64 | let article = {
65 | id: row.postId,
66 | title: row.title,
67 | subtitle: row.subtitle,
68 | imageName: row.image,
69 | image: this.driveExportUrl + row.imageId,
70 | categoryId,
71 | lastUpdated: row.lastUpdated,
72 | date: this.formatDate(row.lastUpdated),
73 | uri: `/articles/${row.postId}/${this.slug(
74 | row.title,
75 | 'article'
76 | )}`
77 | }
78 |
79 | if (existingCategory) {
80 | categories[categoryId].articles.push(row.postId)
81 | } else {
82 | category = {
83 | id: categoryId,
84 | title: row.category,
85 | imageName: row.image,
86 | image: this.driveExportUrl + row.imageId,
87 | articles: [row.postId],
88 | uri: `/categories/${categoryId}`
89 | }
90 | categories[categoryId] = category
91 | }
92 | articles[row.postId] = article
93 | })
94 | return {
95 | articles,
96 | categories
97 | }
98 | })
99 | }
100 |
101 | getArticleHtml(articleId) {
102 | return this.getDocument(articleId).then(doc => {
103 | let styleStart = ''
105 | let splitStyleStart = doc.split(styleStart)
106 | let splitStyleEnd = splitStyleStart[1].split(styleEnd)
107 |
108 | let htmlStart = '' +
122 | splitHtmlEnd[0] +
123 | ''
124 | )
125 | })
126 | }
127 |
128 | slug(str, type = 'type') {
129 | str = str.replace(/^\s+|\s+$/g, '')
130 | str = str.toLowerCase()
131 |
132 | let from = 'ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;'
133 | let to = 'aaaaaeeeeeiiiiooooouuuunc------'
134 | for (let i = 0, l = from.length; i < l; i++) {
135 | str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
136 | }
137 |
138 | str = str
139 | .replace(/[^a-z0-9 -]/g, '')
140 | .replace(/\s+/g, '-')
141 | .replace(/-+/g, '-')
142 |
143 | if (str.length < 4) {
144 | str = type + '_' + str
145 | }
146 | return str
147 | }
148 |
149 | formatDate(lastUpdated) {
150 | var fullDateSplit = lastUpdated.split(' ')
151 | var dateSplit = fullDateSplit[0].split('/')
152 | var day = parseInt(dateSplit[0])
153 | var month = dateSplit[1]
154 | var year = dateSplit[2]
155 | var monthNames = [
156 | 'January',
157 | 'February',
158 | 'March',
159 | 'April',
160 | 'May',
161 | 'June',
162 | 'July',
163 | 'August',
164 | 'September',
165 | 'October',
166 | 'November',
167 | 'December'
168 | ]
169 | var daySuffix = 'th'
170 | switch (day) {
171 | case 1:
172 | daySuffix = 'st'
173 | break
174 | case 2:
175 | daySuffix = 'nd'
176 | break
177 | case 3:
178 | daySuffix = 'rd'
179 | break
180 | }
181 | return day + daySuffix + ' of ' + monthNames[month - 1] + ' ' + year
182 | }
183 | }
184 |
185 | export default new Drive()
186 |
--------------------------------------------------------------------------------
/assets/js/vendors/redux.js:
--------------------------------------------------------------------------------
1 | function objectToString(e){return nativeObjectToString.call(e)}function getRawTag(e){var t=hasOwnProperty.call(e,symToStringTag),n=e[symToStringTag];try{e[symToStringTag]=void 0;var o=!0}catch(e){}var r=nativeObjectToString.call(e);return o&&(t?e[symToStringTag]=n:delete e[symToStringTag]),r}function baseGetTag(e){return null==e?void 0===e?undefinedTag:nullTag:symToStringTag&&symToStringTag in Object(e)?getRawTag(e):objectToString(e)}function overArg(e,t){return function(n){return e(t(n))}}function isObjectLike(e){return null!=e&&"object"==typeof e}function isPlainObject(e){if(!isObjectLike(e)||baseGetTag(e)!=objectTag)return!1;var t=getPrototype(e);if(null===t)return!0;var n=hasOwnProperty.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&funcToString.call(n)==objectCtorString}function symbolObservablePonyfill(e){var t,n=e.Symbol;return"function"==typeof n?n.observable?t=n.observable:(t=n("observable"),n.observable=t):t="@@observable",t}function warning(e){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(e);try{throw new Error(e)}catch(e){}}function applyMiddleware(...e){return t=>(...n)=>{const o=t(...n);let r=o.dispatch,i=[];const c={getState:o.getState,dispatch:(...e)=>r(...e)};return i=e.map(e=>e(c)),r=compose(...i)(o.dispatch),{...o,dispatch:r}}}function bindActionCreator(e,t){return function(){return t(e.apply(this,arguments))}}function bindActionCreators(e,t){if("function"==typeof e)return bindActionCreator(e,t);if("object"!=typeof e||null===e)throw new Error(`bindActionCreators expected an object or a function, instead received ${null===e?"null":typeof e}. `+`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`);const n=Object.keys(e),o={};for(let r=0;re:1===e.length?e[0]:e.reduce((e,t)=>(...n)=>e(t(...n)))}function createStore(e,t,n){function o(){f===u&&(f=u.slice())}function r(){return s}function i(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");let t=!0;return o(),f.push(e),function(){if(!t)return;t=!1,o();const n=f.indexOf(e);f.splice(n,1)}}function c(e){if(!isPlainObject(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(l)throw new Error("Reducers may not dispatch actions.");try{l=!0,s=a(s,e)}finally{l=!1}const t=u=f;for(let e=0;e!t.hasOwnProperty(e)&&!o[e]);return c.forEach(e=>{o[e]=!0}),c.length>0?`Unexpected ${c.length>1?"keys":"key"} `+`"${c.join('", "')}" found in ${i}. `+`Expected to find one of the known reducer keys instead: `+`"${r.join('", "')}". Unexpected keys will be ignored.`:void 0}function assertReducerShape(e){Object.keys(e).forEach(t=>{const n=e[t];if(void 0===n(void 0,{type:ActionTypes.INIT}))throw new Error(`Reducer "${t}" returned undefined during initialization. `+`If the state passed to the reducer is undefined, you must `+`explicitly return the initial state. The initial state may `+`not be undefined. If you don't want to set a value for this reducer, `+`you can use null instead of undefined.`);if(void 0===n(void 0,{type:"@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".")}))throw new Error(`Reducer "${t}" returned undefined when probed with a random type. `+`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" `+`namespace. They are considered private. Instead, you must return the `+`current state for any unknown actions, unless it is undefined, `+`in which case you must return the initial state, regardless of the `+`action type. The initial state may not be undefined, but can be null.`)})}function combineReducers(e){const t=Object.keys(e),n={};for(let o=0;o param
42 | )
43 | }
44 |
45 | render() {
46 | let {
47 | title,
48 | subtitle,
49 | description,
50 | sidebarImage,
51 | showLinks,
52 | children
53 | } = this.props
54 | let { menuVisible } = this.state
55 |
56 | return (
57 |
58 |
66 |
70 |
71 |
72 |
73 |
74 |
75 |
83 |
91 |
99 |
100 |
101 | )
102 | }
103 | }
104 |
105 | function mapStateToProps(state) {
106 | return {
107 | location: getLocation(state)
108 | }
109 | }
110 |
111 | function mapDispatchToProps(dispatch) {
112 | return Object.assign(
113 | bindActionCreators(
114 | {
115 | ...categoryActions
116 | },
117 | dispatch
118 | ),
119 | { dispatch }
120 | )
121 | }
122 |
123 | export default connect(
124 | mapStateToProps,
125 | mapDispatchToProps
126 | )(Page)
127 |
128 | let styles = StyleSheet.create({
129 | page: {
130 | display: 'flex',
131 | width: '100%',
132 | justifyContent: 'flex-end',
133 | overflowX: 'hidden',
134 | maxWidth: '100%'
135 | },
136 | main: {
137 | opacity: 1,
138 | width: '100%',
139 | display: 'block',
140 | transition: 'width linear 750ms',
141 | '@media (min-width: 768px)': {
142 | display: 'block'
143 | },
144 | '@media (min-width: 992px)': {
145 | flexDirection: 'row',
146 | display: 'flex',
147 | justifyContent: 'flex-end'
148 | },
149 | margin: 0,
150 | padding: 0,
151 | overflowX: 'hidden',
152 | maxWidth: '100%'
153 | },
154 | mainNarrow: {
155 | margin: 0,
156 | width: '100%',
157 | '@media (min-width: 768px)': {
158 | width: '60%',
159 | display: 'block'
160 | },
161 | '@media (min-width: 992px)': {
162 | display: 'block',
163 | width: '70%'
164 | },
165 | '@media (min-width: 1200px)': {
166 | width: '75%',
167 | flexDirection: 'row',
168 | display: 'flex',
169 | justifyContent: 'flex-end'
170 | }
171 | },
172 | content: {
173 | padding: '5rem',
174 | overflowX: 'hidden',
175 | maxWidth: '100%',
176 | transition: 'width linear 750ms',
177 | width: '100%',
178 | marginLeft: 0,
179 | '@media (min-width: 768px)': {
180 | width: '100%'
181 | },
182 | '@media (min-width: 992px)': {
183 | width: '60%'
184 | },
185 | '@media (min-width: 1200px)': {
186 | width: '60%'
187 | }
188 | },
189 | contentNarrow: {
190 | width: '100%',
191 | marginLeft: 0,
192 | '@media (min-width: 768px)': {
193 | width: '100%'
194 | },
195 | '@media (min-width: 992px)': {
196 | width: '100%'
197 | },
198 | '@media (min-width: 1200px)': {
199 | width: '52%'
200 | }
201 | },
202 | menuBurger: {
203 | position: 'fixed',
204 | top: '1.5rem',
205 | left: '1.5rem',
206 | zIndex: '15',
207 | borderRadius: 5,
208 | height: '4rem',
209 | width: '4rem',
210 | background: '#333',
211 | paddingTop: 8,
212 | cursor: 'pointer',
213 | borderBottom: '0 transparent',
214 | boxShadow: '#948b8b 2px 2px 10px',
215 | color: '#fff',
216 | display: 'flex',
217 | flexDirection: 'column',
218 | alignItems: 'center',
219 | outline: 0,
220 | border: 0,
221 | ':hover': {
222 | color: '#fff',
223 | outline: 0,
224 | background: '#999'
225 | },
226 | ':focus': {
227 | outline: 0
228 | }
229 | },
230 | bar: {
231 | height: '0.5rem',
232 | width: '2.8rem',
233 | display: 'block',
234 | margin: '0 6px 5px',
235 | background: '#fff',
236 | borderRadius: '0.3rem'
237 | }
238 | })
239 |
--------------------------------------------------------------------------------
/src/components/layout/menu.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { Link } from 'react-router-dom'
4 | import { connect } from 'react-redux'
5 | import { bindActionCreators } from 'redux'
6 |
7 | class Menu extends Component {
8 | constructor() {
9 | super()
10 | this.toggleCategory = this.toggleCategory.bind(this)
11 |
12 | this.state = {
13 | activeCategory: false
14 | }
15 | }
16 |
17 | toggleCategory(event) {
18 | let { activeCategory } = this.state
19 | let category = event.target.dataset.category
20 | this.setState({
21 | activeCategory: category !== activeCategory ? category : false
22 | })
23 | }
24 |
25 | componentWillReceiveProps(nextProps) {
26 | let { categories } = nextProps
27 | if (categories && Object.values(categories).length) {
28 | this.setState({
29 | activeCategory: Object.values(categories)[0]['id']
30 | })
31 | }
32 | }
33 |
34 | render() {
35 | let { categories, articles, menuVisible } = this.props
36 | let { activeCategory } = this.state
37 | return (
38 |
125 | )
126 | }
127 | }
128 |
129 | function mapStateToProps(state) {
130 | return {
131 | categories: state.category.categories,
132 | articles: state.article.articles
133 | }
134 | }
135 |
136 | function mapDispatchToProps(dispatch) {
137 | return Object.assign(bindActionCreators({}, dispatch), { dispatch })
138 | }
139 |
140 | export default connect(
141 | mapStateToProps,
142 | mapDispatchToProps
143 | )(Menu)
144 |
145 | let styles = StyleSheet.create({
146 | menu: {
147 | backgroundColor: '#333',
148 | overflow: 'hidden',
149 | zIndex: 10,
150 | display: 'block',
151 | top: 0,
152 | left: 0,
153 | height: '100%',
154 | boxShadow: '#000 2px 2px 10px',
155 | paddingTop: '5rem',
156 | transition: 'opacity linear 750ms,width linear 750ms',
157 | width: 0,
158 | opacity: 0,
159 | paddingRight: 0,
160 | position: 'fixed'
161 | },
162 | menuOpen: {
163 | opacity: 1,
164 | width: '100%',
165 | '@media (min-width: 768px)': {
166 | width: '40%'
167 | },
168 | '@media (min-width: 992px)': {
169 | width: '30%'
170 | },
171 | '@media (min-width: 1200px)': {
172 | width: '25%'
173 | }
174 | },
175 | icon: {
176 | padding: '0 20px',
177 | color: '#DADADA',
178 | fontSize: '1.6rem'
179 | },
180 | list: {
181 | padding: '10px 0',
182 | fontSize: '1.6rem',
183 | marginBottom: 20,
184 | marginTop: 0
185 | },
186 | item: {
187 | margin: 0,
188 | listStyle: 'none',
189 | padding: '10px 0',
190 | fontSize: '1.6rem'
191 | },
192 | itemLink: {
193 | color: '#DADADA',
194 | fontWeight: 500,
195 | fontSize: 'large',
196 | borderBottom: '0 transparent',
197 | backgroundColor: 'transparent',
198 | outline: 0,
199 | border: 0,
200 | cursor: 'pointer',
201 | ':hover': {
202 | color: '#fff',
203 | outline: 0
204 | },
205 | ':focus': {
206 | outline: 0
207 | },
208 | fontFamily: 'Arial'
209 | },
210 | separator: {
211 | margin: '20px auto',
212 | display: 'block',
213 | border: '1px solid #dededc',
214 | height: 0,
215 | width: '40%'
216 | },
217 | subList: {
218 | marginLeft: '15px',
219 | paddingTop: 0,
220 | paddingBottom: 0,
221 | position: 'relative',
222 | padding: '10px 0',
223 | marginBottom: 0,
224 | fontSize: '1.6rem',
225 | marginTop: 0
226 | },
227 | subItem: {
228 | padding: 0,
229 | height: 0,
230 | overflow: 'hidden',
231 | opacity: '.1',
232 | position: 'relative',
233 | fontSize: 'small',
234 | margin: 0,
235 | listStyle: 'none',
236 | transition: 'opacity ease 750ms,height linear 750ms'
237 | },
238 | subItemExpanded: {
239 | opacity: 1,
240 | height: '4.5rem',
241 | transition: 'opacity ease 750ms,height linear 750ms'
242 | },
243 | subItemLink: {
244 | fontSize: 'medium',
245 | position: 'relative',
246 | color: '#DADADA',
247 | fontWeight: 500,
248 | borderBottom: '0 transparent',
249 | textDecoration: 'none',
250 | backgroundColor: 'transparent',
251 | fontStyle: 'normal',
252 | top: '10px',
253 | ':hover': {
254 | borderBottom: 'none',
255 | color: '#fff'
256 | },
257 | fontFamily: 'Arial'
258 | }
259 | })
260 |
--------------------------------------------------------------------------------
/assets/js/vendors/prop-types.js:
--------------------------------------------------------------------------------
1 | function createCommonjsModule(e,n){return n={exports:{}},e(n,n.exports),n.exports}function makeEmptyFunction(e){return function(){return e}}function invariant(e,n,r,t,o,a,i,u){if(validateFormat(n),!e){var c;if(void 0===n)c=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var p=[r,t,o,a,i,u],s=0;(c=new Error(n.replace(/%s/g,function(){return p[s++]}))).name="Invariant Violation"}throw c.framesToPop=1,c}}function toObject(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function shouldUseNative(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var n={},r=0;r<10;r++)n["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(n).map(function(e){return n[e]}).join(""))return!1;var t={};return"abcdefghijklmnopqrst".split("").forEach(function(e){t[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},t)).join("")}catch(e){return!1}}function checkPropTypes(e,n,r,t,o){for(var a in e)if(e.hasOwnProperty(a)){var i;try{invariant$1("function"==typeof e[a],"%s: %s type `%s` is invalid; it must be a function, usually from the `prop-types` package, but received `%s`.",t||"React class",r,a,typeof e[a]),i=e[a](n,a,t,r,null,ReactPropTypesSecret$1)}catch(e){i=e}if(warning$1(!i||i instanceof Error,"%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",t||"React class",r,a,typeof i),i instanceof Error&&!(i.message in loggedTypeFailures)){loggedTypeFailures[i.message]=!0;var u=o?o():"";warning$1(!1,"Failed %s type: %s%s",r,i.message,null!=u?u:"")}}}var emptyFunction=function(){};emptyFunction.thatReturns=makeEmptyFunction,emptyFunction.thatReturnsFalse=makeEmptyFunction(!1),emptyFunction.thatReturnsTrue=makeEmptyFunction(!0),emptyFunction.thatReturnsNull=makeEmptyFunction(null),emptyFunction.thatReturnsThis=function(){return this},emptyFunction.thatReturnsArgument=function(e){return e};var emptyFunction_1=emptyFunction,validateFormat=function(e){};validateFormat=function(e){if(void 0===e)throw new Error("invariant requires an error message argument")};var invariant_1=invariant,warning=emptyFunction_1,printWarning=function(e){for(var n=arguments.length,r=Array(n>1?n-1:0),t=1;t2?r-2:0),o=2;o>",v={array:i("array"),bool:i("boolean"),func:i("function"),number:i("number"),object:i("object"),string:i("string"),symbol:i("symbol"),any:a(emptyFunction_1.thatReturnsNull),arrayOf:function(e){return a(function(n,r,t,a,i){if("function"!=typeof e)return new o("Property `"+i+"` of component `"+t+"` has invalid PropType notation inside arrayOf.");var u=n[r];if(!Array.isArray(u))return new o("Invalid "+a+" `"+i+"` of type `"+p(u)+"` supplied to `"+t+"`, expected an array.");for(var c=0;c (
8 |
104 | )
105 |
106 | export default pure(Footer)
107 |
108 | let styles = StyleSheet.create({
109 | footer: {
110 | width: '100%',
111 | backgroundColor: '#F5F5F5',
112 | borderTop: 'solid 1px #E9E9E9',
113 | padding: '3rem'
114 | },
115 | footerTop: {
116 | display: 'flex',
117 | flexDirection: 'column',
118 | justifyContent: 'space-evenly',
119 | '@media (min-width: 768px)': {
120 | flexDirection: 'row'
121 | },
122 | '@media (min-width: 992px)': {
123 | flexDirection: 'row'
124 | },
125 | marginBottom: '3rem',
126 | alignItems: 'center'
127 | },
128 | topNarrow: {
129 | '@media (min-width: 768px)': {
130 | flexDirection: 'column'
131 | },
132 | '@media (min-width: 992px)': {
133 | flexDirection: 'row'
134 | }
135 | },
136 | profile: {
137 | width: '6rem',
138 | padding: 0,
139 | border: 0,
140 | borderRadius: '50%',
141 | height: '6rem',
142 | marginBottom: '1rem'
143 | },
144 | credits: {
145 | width: '100%',
146 | marginBottom: '1rem',
147 | '@media (min-width: 768px)': {
148 | borderRight: 'solid 4px #E9E9E9',
149 | padding: 0,
150 | width: '50%'
151 | },
152 | '@media (min-width: 992px)': {
153 | borderRight: 'solid 4px #E9E9E9',
154 | padding: 0,
155 | width: '50%'
156 | }
157 | },
158 | creditsNarrow: {
159 | '@media (min-width: 768px)': {
160 | borderRight: 'none',
161 | borderBottom: 'solid 4px #E9E9E9',
162 | padding: 0,
163 | width: '100%'
164 | },
165 | '@media (min-width: 992px)': {
166 | borderBottom: 'none',
167 | borderRight: 'solid 4px #E9E9E9',
168 | padding: 0,
169 | width: '50%'
170 | }
171 | },
172 | p: {
173 | paddingRight: '2rem',
174 | letterSpacing: '2px',
175 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
176 | fontSize: '1.1rem',
177 | textTransform: 'uppercase',
178 | color: '#000000',
179 | lineHeight: '30px',
180 | margin: 0
181 | },
182 | underline: {
183 | borderBottom: 'solid 1px #222'
184 | },
185 | blueLink: {
186 | color: '#337ab7',
187 | textDecoration: 'none'
188 | },
189 | share: {},
190 | social: {},
191 | socialLinks: {
192 | display: 'flex'
193 | },
194 | socialIcon: {
195 | margin: '0 10px',
196 | display: 'inline-block',
197 | color: '#ccc',
198 | borderBottom: 'solid 1px #fAfafa',
199 | textDecoration: 'none',
200 | backgroundColor: 'transparent',
201 | fontSize: '2.4rem',
202 | ':hover': {
203 | color: '#aaa'
204 | }
205 | },
206 | footerBottom: {
207 | display: 'flex',
208 | flexWrap: 'wrap',
209 | justifyContent: 'end'
210 | },
211 | otherArticle: {
212 | display: 'flex',
213 | justifyContent: 'center',
214 | backgroundPosition: 'center center',
215 | backgroundSize: 'cover',
216 | height: '20rem',
217 | pointerEvents: 'auto',
218 | width: '80%',
219 | position: 'relative',
220 | marginRight: '10%',
221 | marginLeft: '10%',
222 | marginBottom: '3rem',
223 | '@media (min-width: 768px)': {
224 | width: '40%',
225 | marginRight: '5%',
226 | marginLeft: '5%'
227 | },
228 | '@media (min-width: 992px)': {
229 | width: '27.3%',
230 | marginRight: '3%',
231 | marginLeft: '3%'
232 | },
233 | '@media (min-width: 1200px)': {
234 | width: '27.3%',
235 | marginRight: '3%',
236 | marginLeft: '3%'
237 | }
238 | },
239 | otherArticleNarrow: {
240 | width: '80%',
241 | marginRight: '10%',
242 | marginLeft: '10%',
243 | '@media (min-width: 768px)': {
244 | width: '80%',
245 | marginRight: '10%',
246 | marginLeft: '10%'
247 | },
248 | '@media (min-width: 992px)': {
249 | width: '40%',
250 | marginRight: '5%',
251 | marginLeft: '5%'
252 | },
253 | '@media (min-width: 1200px)': {
254 | width: '27.3%',
255 | marginRight: '3%',
256 | marginLeft: '3%'
257 | }
258 | },
259 |
260 | overlay: {
261 | position: 'absolute',
262 | width: '100%',
263 | height: '100%',
264 | zIndex: 2,
265 | backgroundColor: 'rgba(50,50,50,.5)',
266 | top: 0,
267 | left: 0,
268 | pointerEvents: 'none'
269 | },
270 | otherArticleTitle: {
271 | color: '#E9E9E9',
272 | marginRight: '5px',
273 | cursor: 'pointer',
274 | borderBottom: 'solid 1px #fAfafa',
275 | textDecoration: 'none',
276 | backgroundColor: 'transparent',
277 | fontSize: 'large',
278 | letterSpacing: '2px',
279 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
280 | textTransform: 'uppercase',
281 | lineHeight: '30px',
282 | margin: 0,
283 | alignSelf: 'center',
284 | zIndex: 5,
285 | ':hover': {
286 | color: '#fff'
287 | }
288 | }
289 | })
290 |
--------------------------------------------------------------------------------
/src/routes/article.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Helmet } from 'react-helmet'
3 | import { StyleSheet, css } from 'aphrodite'
4 | import { bindActionCreators } from 'redux'
5 | import { connect } from 'react-redux'
6 |
7 | import { getLocation } from 'route/selectors'
8 | import * as categoryActions from 'category/actionCreators'
9 | import * as articleActions from 'article/actionCreators'
10 | import DisqusThread from 'components/disqus/disqusThread'
11 |
12 | import Menu from 'components/layout/menu'
13 | import blocks from 'styles/blocks'
14 | import Footer from 'components/layout/footer'
15 |
16 | class Article extends Component {
17 | static readyOnActions(dispatch, activeArticleId) {
18 | return Promise.all([
19 | dispatch(categoryActions.fetchCategoriesIfNeeded()),
20 | dispatch(articleActions.fetchArticleIfNeeded(activeArticleId))
21 | ])
22 | }
23 |
24 | constructor() {
25 | super()
26 | this.toggleMenu = this.toggleMenu.bind(this)
27 | this.state = {
28 | menuVisible: !(
29 | typeof window !== 'undefined' && window.innerWidth < 769
30 | )
31 | }
32 | }
33 |
34 | componentDidMount() {
35 | let { match } = this.props
36 | let activeArticleId = match.params.articleId
37 | Article.readyOnActions(this.props.dispatch, activeArticleId)
38 | }
39 |
40 | componentWillReceiveProps(nextProps) {
41 | let { match } = this.props
42 | let activeArticleId = match.params.articleId
43 | if (nextProps.match.params.articleId !== activeArticleId) {
44 | Article.readyOnActions(
45 | this.props.dispatch,
46 | nextProps.match.params.articleId
47 | ).then(loaded => {
48 | let element = document.getElementById('article-header')
49 | element.scrollIntoView()
50 | })
51 | }
52 | }
53 |
54 | toggleMenu() {
55 | let { menuVisible } = this.state
56 | this.setState(
57 | {
58 | menuVisible: !menuVisible
59 | },
60 | param => param
61 | )
62 | }
63 |
64 | render() {
65 | let { texts, articles, categories, match } = this.props
66 | let activeArticleId = match.params.articleId
67 | let activeArticle = { title: '', id: activeArticleId },
68 | activeText,
69 | category = { title: '', uri: '/' }
70 | if (
71 | typeof articles[activeArticleId] !== 'undefined' &&
72 | typeof texts[activeArticleId] !== 'undefined' &&
73 | typeof categories[articles[activeArticleId].categoryId] !==
74 | 'undefined'
75 | ) {
76 | activeArticle = articles[activeArticleId]
77 | activeText = texts[activeArticleId]
78 | category = categories[activeArticle.categoryId]
79 | }
80 |
81 | let { menuVisible } = this.state
82 |
83 | return (
84 |
85 |
93 |
102 |
103 |
110 |
118 |
124 |
125 | {activeArticle.title}
126 |
127 |
128 | {activeArticle.subtitle}
129 |
130 |
134 |
138 |
139 |
145 |
146 |
147 | )
148 | }
149 | }
150 |
151 | function mapStateToProps(state) {
152 | return {
153 | location: getLocation(state),
154 | articles: state.article.articles,
155 | categories: state.category.categories,
156 | texts: state.article.texts
157 | }
158 | }
159 |
160 | function mapDispatchToProps(dispatch) {
161 | return Object.assign(
162 | bindActionCreators(
163 | {
164 | ...categoryActions
165 | },
166 | dispatch
167 | ),
168 | { dispatch }
169 | )
170 | }
171 |
172 | export default connect(
173 | mapStateToProps,
174 | mapDispatchToProps
175 | )(Article)
176 |
177 | const opacityKeyframes = {
178 | from: {
179 | opacity: 0
180 | },
181 |
182 | to: {
183 | opacity: 1
184 | }
185 | }
186 |
187 | let styles = StyleSheet.create({
188 | hero: {
189 | position: 'relative',
190 | display: 'block',
191 | height: '15rem',
192 | width: '100%',
193 | backgroundPosition: 'center',
194 | backgroundRepeat: 'no-repeat',
195 | backgroundSize: 'cover',
196 | '@media (min-width: 768px)': {
197 | height: '30rem'
198 | },
199 | overflowX: 'hidden',
200 | maxWidth: '100%'
201 | },
202 | title: {
203 | paddingTop: '20pt',
204 | color: '#000000',
205 | fontSize: '20pt',
206 | paddingBottom: '6pt',
207 | fontFamily: '"Arial"',
208 | lineHeight: '1.15',
209 | pageBreakAfter: 'avoid',
210 | orphans: 2,
211 | widows: 2,
212 | textAlign: 'left',
213 | letterSpacing: '-1pt',
214 | marginTop: '20px',
215 | margin: '.67em 0',
216 | fontWeight: 700,
217 | marginBottom: '10px'
218 | },
219 | p: {
220 | margin: 0,
221 | paddingTop: '0pt',
222 | color: '#666666',
223 | fontSize: '15pt',
224 | paddingBottom: '16pt',
225 | fontFamily: '"Arial"',
226 | lineHeight: '1.15',
227 | pageBreakAfter: 'avoid',
228 | orphans: 2,
229 | widows: 2,
230 | textAlign: 'left'
231 | },
232 | text: {},
233 | page: {
234 | display: 'flex',
235 | width: '100%',
236 | justifyContent: 'flex-end',
237 | overflowX: 'hidden',
238 | maxWidth: '100%'
239 | },
240 | main: {
241 | opacity: 1,
242 | width: '100%',
243 | overflowX: 'hidden',
244 | display: 'block',
245 | transition: 'width linear 750ms',
246 | margin: 0,
247 | padding: 0,
248 | animationName: [opacityKeyframes],
249 | animationDuration: '1s, 1s',
250 | animationIterationCount: 1,
251 | maxWidth: '100%'
252 | },
253 | mainNarrow: {
254 | '@media (min-width: 768px)': {
255 | width: '60%'
256 | },
257 | '@media (min-width: 992px)': {
258 | width: '70%'
259 | },
260 | '@media (min-width: 1200px)': {
261 | width: '75%'
262 | }
263 | },
264 | content: {
265 | display: 'block',
266 | width: '100%',
267 | padding: '3rem 8%',
268 | '@media (min-width: 768px)': {
269 | padding: '3rem 16%'
270 | },
271 | '@media (min-width: 992px)': {
272 | padding: '3rem 18%'
273 | },
274 | '@media (min-width: 1200px)': {
275 | padding: '3rem 24%'
276 | },
277 | overflowX: 'hidden',
278 | maxWidth: '100%'
279 | },
280 | contentNarrow: {
281 | display: 'block',
282 | width: '100%',
283 | padding: '3rem 8%',
284 | '@media (min-width: 768px)': {
285 | padding: '3rem 8%'
286 | },
287 | '@media (min-width: 992px)': {
288 | padding: '3rem 12%'
289 | },
290 | '@media (min-width: 1200px)': {
291 | padding: '3rem 20%'
292 | }
293 | },
294 |
295 | menuBurger: {
296 | position: 'fixed',
297 | top: '1.5rem',
298 | left: '1.5rem',
299 | zIndex: '15',
300 | borderRadius: 5,
301 | height: '4rem',
302 | width: '4rem',
303 | background: '#333',
304 | paddingTop: 8,
305 | cursor: 'pointer',
306 | borderBottom: '0 transparent',
307 | boxShadow: '#948b8b 2px 2px 10px',
308 | color: '#fff',
309 | display: 'flex',
310 | flexDirection: 'column',
311 | alignItems: 'center',
312 | outline: 0,
313 | border: 0,
314 | ':hover': {
315 | color: '#fff',
316 | outline: 0,
317 | background: '#999'
318 | },
319 | ':focus': {
320 | outline: 0
321 | }
322 | },
323 | bar: {
324 | height: '0.5rem',
325 | width: '2.8rem',
326 | display: 'block',
327 | margin: '0 6px 5px',
328 | background: '#fff',
329 | borderRadius: '0.3rem'
330 | }
331 | })
332 |
--------------------------------------------------------------------------------
/assets/js/vendors/react-helmet.js:
--------------------------------------------------------------------------------
1 | function isUndefinedOrNull(t){return null===t||void 0===t}function isBuffer(t){return!(!t||"object"!=typeof t||"number"!=typeof t.length)&&("function"==typeof t.copy&&"function"==typeof t.slice&&!(t.length>0&&"number"!=typeof t[0]))}function objEquiv(t,e,r){var n,s;if(isUndefinedOrNull(t)||isUndefinedOrNull(e))return!1;if(t.prototype!==e.prototype)return!1;if(isArguments(t))return!!isArguments(e)&&(t=pSlice.call(t),e=pSlice.call(e),deepEqual(t,e,r));if(isBuffer(t)){if(!isBuffer(e))return!1;if(t.length!==e.length)return!1;for(n=0;n=0;n--)if(o[n]!=T[n])return!1;for(n=o.length-1;n>=0;n--)if(s=o[n],!deepEqual(t[s],e[s],r))return!1;return typeof t==typeof e}function withSideEffect(t,e,r){function n(t){return t.displayName||t.name||"Component"}function s(t,e){for(let r in t)if(!(r in e))return!0;for(let r in e)if(t[r]!==e[r])return!0;return!1}if("function"!=typeof t)throw new Error("Expected reducePropsToState to be a function.");if("function"!=typeof e)throw new Error("Expected handleStateChangeOnClient to be a function.");if(void 0!==r&&"function"!=typeof r)throw new Error("Expected mapStateOnServer to either be undefined or a function.");return function(o){function T(){a=t(i.map(function(t){return t.props})),l.canUseDOM?e(a):r&&(a=r(a))}if("function"!=typeof o)throw new Error("Expected WrappedComponent to be a React component.");let a,i=[];class l extends Component{shouldComponentUpdate(t){let{children:e,...r}=t;return e&&e.length&&(r.children=e),s(r,this.props)}componentWillMount(){i.push(this),T()}componentDidUpdate(){T()}componentWillUnmount(){const t=i.indexOf(this);i.splice(t,1),T()}render(){return h(o,{...this.props})}}return l.displayName=`SideEffect(${n(o)})`,l.canUseDOM=!("undefined"==typeof window||!window.document||!window.document.createElement),l.peek=(()=>a),l.rewind=(()=>{if(l.canUseDOM)throw new Error("You may only call rewind() on the server. Call peek() to read the current state.");let t=a;return a=void 0,i=[],t}),l}}import{h,Component}from "./react.js";var pSlice=Array.prototype.slice,isArguments=function(t){return"[object Arguments]"==Object.prototype.toString.call(t)},deepEqual=function(t, e, r){return r||(r={}),t===e||(t instanceof Date&&e instanceof Date?t.getTime()===e.getTime():!t||!e||"object"!=typeof t&&"object"!=typeof e?r.strict?t===e:t==e:objEquiv(t,e,r))};export const TAG_NAMES={HTML:"htmlAttributes",TITLE:"title",BASE:"base",META:"meta",LINK:"link",SCRIPT:"script",NOSCRIPT:"noscript",STYLE:"style"};export const TAG_PROPERTIES={NAME:"name",CHARSET:"charset",HTTPEQUIV:"http-equiv",REL:"rel",HREF:"href",PROPERTY:"property",SRC:"src",INNER_HTML:"innerHTML",CSS_TEXT:"cssText",ITEM_PROP:"itemprop"};export const PREACT_TAG_MAP={charset:"charSet","http-equiv":"httpEquiv",itemprop:"itemProp",class:"className"};const HELMET_ATTRIBUTE="data-preact-helmet",encodeSpecialCharacters=t=>String(t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),getInnermostProperty=(t,e)=>{for(let r=t.length-1;r>=0;r--){const n=t[r];if(n[e])return n[e]}return null},getTitleFromPropsList=t=>{const e=getInnermostProperty(t,"title"),r=getInnermostProperty(t,"titleTemplate");if(r&&e)return r.replace(/%s/g,()=>e);const n=getInnermostProperty(t,"defaultTitle");return e||n||""},getOnChangeClientState=t=>getInnermostProperty(t,"onChangeClientState")||(()=>{}),getAttributesFromPropsList=(t,e)=>e.filter(e=>void 0!==e[t]).map(e=>e[t]).reduce((t,e)=>({...t,...e}),{}),getBaseTagFromPropsList=(t,e)=>e.filter(t=>void 0!==t[TAG_NAMES.BASE]).map(t=>t[TAG_NAMES.BASE]).reverse().reduce((e,r)=>{if(!e.length){const n=Object.keys(r);for(let s=0;s{const n={};return r.filter(e=>void 0!==e[t]).map(e=>e[t]).reverse().reduce((t,r)=>{const s={};r.filter(t=>{let r;const o=Object.keys(t);for(let n=0;nt.push(e));const o=Object.keys(s);for(let t=0;t{document.title=t||document.title,updateAttributes(TAG_NAMES.TITLE,e)},updateAttributes=(t,e)=>{const r=document.getElementsByTagName(t)[0],n=r.getAttribute(HELMET_ATTRIBUTE),s=n?n.split(","):[],o=[].concat(s),T=Object.keys(e);for(let t=0;t=0;t--)r.removeAttribute(o[t]);s.length===o.length?r.removeAttribute(HELMET_ATTRIBUTE):r.setAttribute(HELMET_ATTRIBUTE,s.join(","))},updateTags=(t,e)=>{const r=document.head||document.querySelector("head"),n=r.querySelectorAll(`${t}[data-preact-helmet]`),s=Array.prototype.slice.call(n),o=[];let T;return e&&e.length&&e.forEach(e=>{const r=document.createElement(t);for(const t in e)if(e.hasOwnProperty(t))if("innerHTML"===t)r.innerHTML=e.innerHTML;else if("cssText"===t)r.styleSheet?r.styleSheet.cssText=e.cssText:r.appendChild(document.createTextNode(e.cssText));else{const n=void 0===e[t]?"":e[t];r.setAttribute(t,n)}r.setAttribute(HELMET_ATTRIBUTE,"true"),s.some((t,e)=>(T=e,r.isEqualNode(t)))?s.splice(T,1):o.push(r)}),s.forEach(t=>t.parentNode.removeChild(t)),o.forEach(t=>r.appendChild(t)),{oldTags:s,newTags:o}},generateHtmlAttributesAsString=t=>Object.keys(t).reduce((e,r)=>{const n=void 0!==t[r]?`${r}="${t[r]}"`:`${r}`;return e?`${e} ${n}`:n},""),generateTitleAsString=(t,e,r)=>{const n=generateHtmlAttributesAsString(r);return n?`<${t} data-preact-helmet ${n}>${encodeSpecialCharacters(e)}${t}>`:`<${t} data-preact-helmet>${encodeSpecialCharacters(e)}${t}>`},generateTagsAsString=(t,e)=>e.reduce((e,r)=>{const n=Object.keys(r).filter(t=>!("innerHTML"===t||"cssText"===t)).reduce((t,e)=>{const n=void 0===r[e]?e:`${e}="${encodeSpecialCharacters(r[e])}"`;return t?`${t} ${n}`:n},""),s=r.innerHTML||r.cssText||"",o=-1===[TAG_NAMES.NOSCRIPT,TAG_NAMES.SCRIPT,TAG_NAMES.STYLE].indexOf(t);return`${e}<${t} data-preact-helmet ${n}${o?`>`:`>${s}${t}>`}`},""),generateTitleAsPreactComponent=(t,e,r)=>{const n={key:e,[HELMET_ATTRIBUTE]:!0},s=Object.keys(r).reduce((t,e)=>(t[e]=r[e],t),n);return[h(TAG_NAMES.TITLE,s,e)]},generateTagsAsPreactComponent=(t,e)=>e.map((e,r)=>{const n={key:r,[HELMET_ATTRIBUTE]:!0};return Object.keys(e).forEach(t=>{const r=t;if("innerHTML"===r||"cssText"===r){const t=e.innerHTML||e.cssText;n.dangerouslySetInnerHTML={__html:t}}else n[r]=e[t]}),h(t,n)}),getMethodsForTag=(t,e)=>{switch(t){case TAG_NAMES.TITLE:return{toComponent:()=>generateTitleAsPreactComponent(0,e.title,e.titleAttributes),toString:()=>generateTitleAsString(t,e.title,e.titleAttributes)};case TAG_NAMES.HTML:return{toComponent:()=>e,toString:()=>generateHtmlAttributesAsString(e)};default:return{toComponent:()=>generateTagsAsPreactComponent(t,e),toString:()=>generateTagsAsString(t,e)}}},mapStateOnServer=({htmlAttributes:t,title:e,titleAttributes:r,baseTag:n,metaTags:s,linkTags:o,scriptTags:T,noscriptTags:a,styleTags:i})=>({htmlAttributes:getMethodsForTag(TAG_NAMES.HTML,t),title:getMethodsForTag(TAG_NAMES.TITLE,{title:e,titleAttributes:r}),base:getMethodsForTag(TAG_NAMES.BASE,n),meta:getMethodsForTag(TAG_NAMES.META,s),link:getMethodsForTag(TAG_NAMES.LINK,o),script:getMethodsForTag(TAG_NAMES.SCRIPT,T),noscript:getMethodsForTag(TAG_NAMES.NOSCRIPT,a),style:getMethodsForTag(TAG_NAMES.STYLE,i)}),Helmet=t=>{class e extends Component{static set canUseDOM(e){t.canUseDOM=e}shouldComponentUpdate(t){const e={...t};return e.children&&e.children.length||delete e.children,!deepEqual(this.props,e)}render(){return h(t,{...this.props})}}return e.peek=((...e)=>t.peek(...e)),e.rewind=(()=>{let e=t.rewind();return e||(e=mapStateOnServer({htmlAttributes:{},title:"",titleAttributes:{},baseTag:[],metaTags:[],linkTags:[],scriptTags:[],noscriptTags:[],styleTags:[]})),e}),e},reducePropsToState=t=>({htmlAttributes:getAttributesFromPropsList(TAG_NAMES.HTML,t),title:getTitleFromPropsList(t),titleAttributes:getAttributesFromPropsList("titleAttributes",t),baseTag:getBaseTagFromPropsList([TAG_PROPERTIES.HREF],t),metaTags:getTagsFromPropsList(TAG_NAMES.META,[TAG_PROPERTIES.NAME,TAG_PROPERTIES.CHARSET,TAG_PROPERTIES.HTTPEQUIV,TAG_PROPERTIES.PROPERTY,TAG_PROPERTIES.ITEM_PROP],t),linkTags:getTagsFromPropsList(TAG_NAMES.LINK,[TAG_PROPERTIES.REL,TAG_PROPERTIES.HREF],t),scriptTags:getTagsFromPropsList(TAG_NAMES.SCRIPT,[TAG_PROPERTIES.SRC,TAG_PROPERTIES.INNER_HTML],t),noscriptTags:getTagsFromPropsList(TAG_NAMES.NOSCRIPT,[TAG_PROPERTIES.INNER_HTML],t),styleTags:getTagsFromPropsList(TAG_NAMES.STYLE,[TAG_PROPERTIES.CSS_TEXT],t),onChangeClientState:getOnChangeClientState(t)}),handleClientStateChange=t=>{const{htmlAttributes:e,title:r,titleAttributes:n,baseTag:s,metaTags:o,linkTags:T,scriptTags:a,noscriptTags:i,styleTags:l,onChangeClientState:c}=t;updateAttributes("html",e),updateTitle(r,n);const g={baseTag:updateTags(TAG_NAMES.BASE,s),metaTags:updateTags(TAG_NAMES.META,o),linkTags:updateTags(TAG_NAMES.LINK,T),scriptTags:updateTags(TAG_NAMES.SCRIPT,a),noscriptTags:updateTags(TAG_NAMES.NOSCRIPT,i),styleTags:updateTags(TAG_NAMES.STYLE,l)},E={},p={};Object.keys(g).forEach(t=>{const{newTags:e,oldTags:r}=g[t];e.length&&(E[t]=e),r.length&&(p[t]=g[t].oldTags)}),c(t,E,p)},NullComponent=()=>null,HelmetSideEffects=withSideEffect(t=>({htmlAttributes:getAttributesFromPropsList(TAG_NAMES.HTML,t),title:getTitleFromPropsList(t),titleAttributes:getAttributesFromPropsList("titleAttributes",t),baseTag:getBaseTagFromPropsList([TAG_PROPERTIES.HREF],t),metaTags:getTagsFromPropsList(TAG_NAMES.META,[TAG_PROPERTIES.NAME,TAG_PROPERTIES.CHARSET,TAG_PROPERTIES.HTTPEQUIV,TAG_PROPERTIES.PROPERTY,TAG_PROPERTIES.ITEM_PROP],t),linkTags:getTagsFromPropsList(TAG_NAMES.LINK,[TAG_PROPERTIES.REL,TAG_PROPERTIES.HREF],t),scriptTags:getTagsFromPropsList(TAG_NAMES.SCRIPT,[TAG_PROPERTIES.SRC,TAG_PROPERTIES.INNER_HTML],t),noscriptTags:getTagsFromPropsList(TAG_NAMES.NOSCRIPT,[TAG_PROPERTIES.INNER_HTML],t),styleTags:getTagsFromPropsList(TAG_NAMES.STYLE,[TAG_PROPERTIES.CSS_TEXT],t),onChangeClientState:getOnChangeClientState(t)}),t=>{const{htmlAttributes:e,title:r,titleAttributes:n,baseTag:s,metaTags:o,linkTags:T,scriptTags:a,noscriptTags:i,styleTags:l,onChangeClientState:c}=t;updateAttributes("html",e),updateTitle(r,n);const g={baseTag:updateTags(TAG_NAMES.BASE,s),metaTags:updateTags(TAG_NAMES.META,o),linkTags:updateTags(TAG_NAMES.LINK,T),scriptTags:updateTags(TAG_NAMES.SCRIPT,a),noscriptTags:updateTags(TAG_NAMES.NOSCRIPT,i),styleTags:updateTags(TAG_NAMES.STYLE,l)},E={},p={};Object.keys(g).forEach(t=>{const{newTags:e,oldTags:r}=g[t];e.length&&(E[t]=e),r.length&&(p[t]=g[t].oldTags)}),c(t,E,p)},mapStateOnServer)(()=>null);let HelmetW=(t=>{class e extends Component{static set canUseDOM(e){t.canUseDOM=e}shouldComponentUpdate(t){const e={...t};return e.children&&e.children.length||delete e.children,!deepEqual(this.props,e)}render(){return h(t,{...this.props})}}return e.peek=((...e)=>t.peek(...e)),e.rewind=(()=>{let e=t.rewind();return e||(e=mapStateOnServer({htmlAttributes:{},title:"",titleAttributes:{},baseTag:[],metaTags:[],linkTags:[],scriptTags:[],noscriptTags:[],styleTags:[]})),e}),e})(HelmetSideEffects);export{HelmetW as Helmet};export default HelmetW;
2 |
--------------------------------------------------------------------------------
/src/routes/contact.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { StyleSheet, css } from 'aphrodite'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import { Link } from 'react-router-dom'
6 |
7 | import { getLocation } from 'route/selectors'
8 | import BaseInput from 'components/form/baseInput'
9 | import Page from 'components/layout/page'
10 | import input from 'styles/input'
11 | import buttons from 'styles/buttons'
12 | import Mail from 'lib/mail'
13 | import conf from 'conf'
14 |
15 | class Contact extends Component {
16 | constructor(props) {
17 | super(props)
18 | this.state = {
19 | values: {
20 | name: {
21 | value: '',
22 | error: false,
23 | required: true
24 | },
25 | email: {
26 | value: '',
27 | error: false,
28 | required: true
29 | },
30 | company: {
31 | value: '',
32 | error: false,
33 | required: false
34 | },
35 | phone: {
36 | value: '',
37 | error: false,
38 | required: false
39 | },
40 | message: {
41 | value: '',
42 | error: false,
43 | required: true
44 | }
45 | },
46 | valid: false,
47 | sent: false
48 | }
49 | this.validateEmail = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/
50 | this.updateFormProperty = this.updateFormProperty.bind(this)
51 | this.sendMessage = this.sendMessage.bind(this)
52 | }
53 |
54 | updateFormProperty(property, value) {
55 | let { values } = this.state
56 | let element = values[property]
57 | let { error, required } = element
58 | if (required && value.length < 4) {
59 | error = '4 characters minimum.'
60 | } else if (property === 'email' && !this.validateEmail.test(value)) {
61 | error = 'Enter valid email.'
62 | } else {
63 | error = false
64 | }
65 | values = {
66 | ...values,
67 | [property]: {
68 | required,
69 | value,
70 | error
71 | }
72 | }
73 | let valid = Object.values(values).every(
74 | item => !item.error && (!item.required || item.value.length > 3)
75 | )
76 | this.setState({
77 | values,
78 | valid
79 | })
80 | }
81 |
82 | sendMessage(e) {
83 | e.preventDefault()
84 | e.stopPropagation()
85 | let { values } = this.state
86 | this.setState(
87 | {
88 | sent: true
89 | },
90 | () => {
91 | let formatted = {}
92 | Object.keys(values).forEach(key => {
93 | formatted[key] = values[key]['value']
94 | })
95 |
96 | Mail.send(formatted)
97 | }
98 | )
99 | }
100 |
101 | render() {
102 | let { name, email, company, phone, message } = this.state.values
103 | let { valid, sent } = this.state
104 |
105 | return (
106 |
116 | Send me an email
117 |
249 |
250 |
251 | About
252 |
253 |
254 |
255 | )
256 | }
257 | }
258 |
259 | function mapStateToProps(state) {
260 | return {
261 | location: getLocation(state)
262 | }
263 | }
264 |
265 | function mapDispatchToProps(dispatch) {
266 | return Object.assign(bindActionCreators({}, dispatch), { dispatch })
267 | }
268 |
269 | export default connect(
270 | mapStateToProps,
271 | mapDispatchToProps
272 | )(Contact)
273 |
274 | let styles = StyleSheet.create({
275 | title: {
276 | fontSize: '2.6rem',
277 | marginTop: '20px',
278 | fontFamily: 'inherit',
279 | fontWeight: 500,
280 | lineHeight: '1.1',
281 | color: 'inherit',
282 | marginBottom: '10px'
283 | },
284 |
285 | label: {
286 | fontSize: '2rem',
287 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
288 | fontWeight: 700,
289 | margin: '15px 0 0'
290 | },
291 | button: {
292 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif'
293 | },
294 | error: {
295 | color: 'red'
296 | },
297 | required: {
298 | ':after': {
299 | color: 'red',
300 | content: '" *"'
301 | }
302 | },
303 | footer: {
304 | padding: '10px 0',
305 | fontSize: '1.4rem',
306 | letterSpacing: '1px',
307 | fontWeight: 700,
308 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif',
309 | textTransform: 'uppercase'
310 | },
311 | contact: {
312 | textDecoration: 'none',
313 | backgroundColor: 'transparent',
314 | color: '#999',
315 | borderBottom: 'none',
316 | fontSize: '1.4rem',
317 | ':hover': {
318 | textDecoration: 'none',
319 | backgroundColor: 'transparent',
320 | color: '#333',
321 | outline: 0,
322 | transition: 'all .4s',
323 | borderBottom: 'none'
324 | }
325 | },
326 | form: {
327 | marginBottom: '5rem'
328 | }
329 | })
330 |
--------------------------------------------------------------------------------
/assets/js/vendors/history.js:
--------------------------------------------------------------------------------
1 | function valueEqual(t,e){if(t===e)return!0;if(null==t||null==e)return!1;if(Array.isArray(t))return Array.isArray(e)&&t.length===e.length&&t.every(function(t,n){return valueEqual(t,e[n])});const n=typeof t;if(n!==typeof e)return!1;if("object"===n){const n=t.valueOf(),a=e.valueOf();if(n!==t||a!==e)return valueEqual(n,a);const o=Object.keys(t),r=Object.keys(e);return o.length===r.length&&o.every(function(n){return valueEqual(t[n],e[n])})}return!1}function isAbsolute(t){return"/"===t.charAt(0)}function spliceOne(t,e){for(let n=e,a=n+1,o=t.length;a=0;t--){const e=a[t];"."===e?spliceOne(a,t):".."===e?(spliceOne(a,t),c++):c&&(spliceOne(a,t),c--)}if(!i)for(;c--;c)a.unshift("..");!i||""===a[0]||a[0]&&isAbsolute(a[0])||a.unshift("");let h=a.join("/");return s&&"/"!==h.substr(-1)&&(h+="/"),h}const warning=function(t,e,n){var a=arguments.length;n=new Array(a>2?a-2:0);for(var o=2;ot.addEventListener?t.addEventListener(e,n,!1):t.attachEvent("on"+e,n);export const removeEventListener=(t,e,n)=>t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent("on"+e,n);export const getConfirmation=(t,e)=>e(window.confirm(t));export const supportsHistory=()=>{const t=window.navigator.userAgent;return(-1===t.indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&(window.history&&"pushState"in window.history)};export const supportsPopStateOnHashChange=()=>-1===window.navigator.userAgent.indexOf("Trident");export const supportsGoWithoutReloadUsingHash=()=>-1===window.navigator.userAgent.indexOf("Firefox");export const isExtraneousPopstateEvent=t=>void 0===t.state&&-1===navigator.userAgent.indexOf("CriOS");export const addLeadingSlash=t=>"/"===t.charAt(0)?t:"/"+t;export const stripLeadingSlash=t=>"/"===t.charAt(0)?t.substr(1):t;export const hasBasename=(t,e)=>new RegExp("^"+e+"(\\/|\\?|#|$)","i").test(t);export const stripBasename=(t,e)=>hasBasename(t,e)?t.substr(e.length):t;export const stripTrailingSlash=t=>"/"===t.charAt(t.length-1)?t.slice(0,-1):t;export const parsePath=t=>{let e=t||"/",n="",a="";const o=e.indexOf("#");-1!==o&&(a=e.substr(o),e=e.substr(0,o));const r=e.indexOf("?");return-1!==r&&(n=e.substr(r),e=e.substr(0,r)),{pathname:e,search:"?"===n?"":n,hash:"#"===a?"":a}};export const createPath=t=>{const{pathname:e,search:n,hash:a}=t;let o=e||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:`?${n}`),a&&"#"!==a&&(o+="#"===a.charAt(0)?a:`#${a}`),o};export const createLocation=(t,e,n,a)=>{let o;"string"==typeof t?(o=parsePath(t)).state=e:(void 0===(o={...t}).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==e&&void 0===o.state&&(o.state=e));try{o.pathname=decodeURI(o.pathname)}catch(t){throw t instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):t}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=resolvePathname(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o};export const locationsAreEqual=(t,e)=>t.pathname===e.pathname&&t.search===e.search&&t.hash===e.hash&&t.key===e.key&&valueEqual(t.state,e.state);const createTransitionManager=()=>{let t=null;let e=[];return{setPrompt:e=>(warning(null==t,"A history supports only one prompt at a time"),t=e,()=>{t===e&&(t=null)}),confirmTransitionTo:(e,n,a,o)=>{if(null!=t){const r="function"==typeof t?t(e,n):t;"string"==typeof r?"function"==typeof a?a(r,o):(warning(!1,"A history needs a getUserConfirmation function in order to use a prompt message"),o(!0)):o(!1!==r)}else o(!0)},appendListener:t=>{let n=!0;const a=(...e)=>{n&&t(...e)};return e.push(a),()=>{n=!1,e=e.filter(t=>t!==a)}},notifyListeners:(...t)=>{e.forEach(e=>e(...t))}}},PopStateEvent="popstate",HashChangeEvent="hashchange",getHistoryState=()=>{try{return window.history.state||{}}catch(t){return{}}};export const createBrowserHistory=(t={})=>{invariant(canUseDOM,"Browser history needs a DOM");const e=window.history,n=supportsHistory(),a=!supportsPopStateOnHashChange(),{forceRefresh:o=!1,getUserConfirmation:r=getConfirmation,keyLength:i=6}=t,s=t.basename?stripTrailingSlash(addLeadingSlash(t.basename)):"",c=t=>{const{key:e,state:n}=t||{},{pathname:a,search:o,hash:r}=window.location;let i=a+o+r;return warning(!s||hasBasename(i,s),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+i+'" to begin with "'+s+'".'),s&&(i=stripBasename(i,s)),createLocation(i,n,e)},h=()=>Math.random().toString(36).substr(2,i),l=createTransitionManager(),d=t=>{Object.assign(L,t),L.length=e.length,l.notifyListeners(L.location,L.action)},p=t=>{isExtraneousPopstateEvent(t)||f(c(t.state))},u=()=>{f(c(getHistoryState()))};let g=!1;const f=t=>{if(g)g=!1,d();else{l.confirmTransitionTo(t,"POP",r,e=>{e?d({action:"POP",location:t}):w(t)})}},w=t=>{const e=L.location;let n=v.indexOf(e.key);-1===n&&(n=0);let a=v.indexOf(t.key);-1===a&&(a=0);const o=n-a;o&&(g=!0,P(o))},m=c(getHistoryState());let v=[m.key];const y=t=>s+createPath(t),P=t=>{e.go(t)};let x=0;const b=t=>{1===(x+=t)?(addEventListener(window,"popstate",p),a&&addEventListener(window,"hashchange",u)):0===x&&(removeEventListener(window,"popstate",p),a&&removeEventListener(window,"hashchange",u))};let E=!1;const L={length:e.length,action:"POP",location:m,createHref:y,push:(t,a)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==a),"You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored");const i=createLocation(t,a,h(),L.location);l.confirmTransitionTo(i,"PUSH",r,t=>{if(!t)return;const a=y(i),{key:r,state:s}=i;if(n)if(e.pushState({key:r,state:s},null,a),o)window.location.href=a;else{const t=v.indexOf(L.location.key),e=v.slice(0,-1===t?0:t+1);e.push(i.key),v=e,d({action:"PUSH",location:i})}else warning(void 0===s,"Browser history cannot push state in browsers that do not support HTML5 history"),window.location.href=a})},replace:(t,a)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==a),"You should avoid providing a 2nd state argument to replace when the 1st argument is a location-like object that already has state; it is ignored");const i=createLocation(t,a,h(),L.location);l.confirmTransitionTo(i,"REPLACE",r,t=>{if(!t)return;const a=y(i),{key:r,state:s}=i;if(n)if(e.replaceState({key:r,state:s},null,a),o)window.location.replace(a);else{const t=v.indexOf(L.location.key);-1!==t&&(v[t]=i.key),d({action:"REPLACE",location:i})}else warning(void 0===s,"Browser history cannot replace state in browsers that do not support HTML5 history"),window.location.replace(a)})},go:P,goBack:()=>P(-1),goForward:()=>P(1),block:(t=!1)=>{const e=l.setPrompt(t);return E||(b(1),E=!0),()=>(E&&(E=!1,b(-1)),e())},listen:t=>{const e=l.appendListener(t);return b(1),()=>{b(-1),e()}}};return L};const HashPathCoders={hashbang:{encodePath:t=>"!"===t.charAt(0)?t:"!/"+stripLeadingSlash(t),decodePath:t=>"!"===t.charAt(0)?t.substr(1):t},noslash:{encodePath:stripLeadingSlash,decodePath:addLeadingSlash},slash:{encodePath:addLeadingSlash,decodePath:addLeadingSlash}},getHashPath=()=>{const t=window.location.href,e=t.indexOf("#");return-1===e?"":t.substring(e+1)},pushHashPath=t=>window.location.hash=t,replaceHashPath=t=>{const e=window.location.href.indexOf("#");window.location.replace(window.location.href.slice(0,e>=0?e:0)+"#"+t)};export const createHashHistory=(t={})=>{invariant(canUseDOM,"Hash history needs a DOM");const e=window.history,n=supportsGoWithoutReloadUsingHash(),{getUserConfirmation:a=getConfirmation,hashType:o="slash"}=t,r=t.basename?stripTrailingSlash(addLeadingSlash(t.basename)):"",{encodePath:i,decodePath:s}=HashPathCoders[o],c=()=>{let t=s(getHashPath());return warning(!r||hasBasename(t,r),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+t+'" to begin with "'+r+'".'),r&&(t=stripBasename(t,r)),createLocation(t)},h=createTransitionManager(),l=t=>{Object.assign(L,t),L.length=e.length,h.notifyListeners(L.location,L.action)};let d=!1,p=null;const u=()=>{const t=getHashPath(),e=i(t);if(t!==e)replaceHashPath(e);else{const t=c(),e=L.location;if(!d&&locationsAreEqual(e,t))return;if(p===createPath(t))return;p=null,g(t)}},g=t=>{if(d)d=!1,l();else{h.confirmTransitionTo(t,"POP",a,e=>{e?l({action:"POP",location:t}):f(t)})}},f=t=>{const e=L.location;let n=y.lastIndexOf(createPath(e));-1===n&&(n=0);let a=y.lastIndexOf(createPath(t));-1===a&&(a=0);const o=n-a;o&&(d=!0,P(o))},w=getHashPath(),m=i(w);w!==m&&replaceHashPath(m);const v=c();let y=[createPath(v)];const P=t=>{warning(n,"Hash history go(n) causes a full page reload in this browser"),e.go(t)};let x=0;const b=t=>{1===(x+=t)?addEventListener(window,"hashchange",u):0===x&&removeEventListener(window,"hashchange",u)};let E=!1;const L={length:e.length,action:"POP",location:v,createHref:t=>"#"+i(r+createPath(t)),push:(t,e)=>{warning(void 0===e,"Hash history cannot push state; it is ignored");const n=createLocation(t,void 0,void 0,L.location);h.confirmTransitionTo(n,"PUSH",a,t=>{if(!t)return;const e=createPath(n),a=i(r+e);if(getHashPath()!==a){p=e,pushHashPath(a);const t=y.lastIndexOf(createPath(L.location)),o=y.slice(0,-1===t?0:t+1);o.push(e),y=o,l({action:"PUSH",location:n})}else warning(!1,"Hash history cannot PUSH the same path; a new entry will not be added to the history stack"),l()})},replace:(t,e)=>{warning(void 0===e,"Hash history cannot replace state; it is ignored");const n=createLocation(t,void 0,void 0,L.location);h.confirmTransitionTo(n,"REPLACE",a,t=>{if(!t)return;const e=createPath(n),a=i(r+e);getHashPath()!==a&&(p=e,replaceHashPath(a));const o=y.indexOf(createPath(L.location));-1!==o&&(y[o]=e),l({action:"REPLACE",location:n})})},go:P,goBack:()=>P(-1),goForward:()=>P(1),block:(t=!1)=>{const e=h.setPrompt(t);return E||(b(1),E=!0),()=>(E&&(E=!1,b(-1)),e())},listen:t=>{const e=h.appendListener(t);return b(1),()=>{b(-1),e()}}};return L};const clamp=(t,e,n)=>Math.min(Math.max(t,e),n);export const createMemoryHistory=(t={})=>{const{getUserConfirmation:e,initialEntries:n=["/"],initialIndex:a=0,keyLength:o=6}=t,r=createTransitionManager(),i=t=>{Object.assign(p,t),p.length=p.entries.length,r.notifyListeners(p.location,p.action)},s=()=>Math.random().toString(36).substr(2,o),c=clamp(a,0,n.length-1),h=n.map(t=>"string"==typeof t?createLocation(t,void 0,s()):createLocation(t,void 0,t.key||s())),l=createPath,d=t=>{const n=clamp(p.index+t,0,p.entries.length-1),a=p.entries[n];r.confirmTransitionTo(a,"POP",e,t=>{t?i({action:"POP",location:a,index:n}):i()})},p={length:h.length,action:"POP",location:h[c],index:c,entries:h,createHref:l,push:(t,n)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==n),"You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored");const a=createLocation(t,n,s(),p.location);r.confirmTransitionTo(a,"PUSH",e,t=>{if(!t)return;const e=p.index+1,n=p.entries.slice(0);n.length>e?n.splice(e,n.length-e,a):n.push(a),i({action:"PUSH",location:a,index:e,entries:n})})},replace:(t,n)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==n),"You should avoid providing a 2nd state argument to replace when the 1st argument is a location-like object that already has state; it is ignored");const a=createLocation(t,n,s(),p.location);r.confirmTransitionTo(a,"REPLACE",e,t=>{t&&(p.entries[p.index]=a,i({action:"REPLACE",location:a}))})},go:d,goBack:()=>d(-1),goForward:()=>d(1),canGo:t=>{const e=p.index+t;return e>=0&&er.setPrompt(t),listen:t=>r.appendListener(t)};return p};
--------------------------------------------------------------------------------