diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..5ec9cc2 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node index.js \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..e0b7371 --- /dev/null +++ b/app.json @@ -0,0 +1,7 @@ +{ + "name": "React HN", + "description": "React-powered Hacker News client", + "repository": "https://github.com/insin/react-hn", + "keywords": ["react", "hacker news", "hn"], + "image": "heroku/nodejs" +} \ No newline at end of file diff --git a/package.json b/package.json index c4bce2b..5d7409d 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,18 @@ "build": "npm run lint && cp node_modules/sw-toolbox/sw-toolbox.js public/sw-toolbox.js && nwb build && npm run precache", "lint": "eslint src", "lint:fix": "eslint --fix .", - "start": "nwb serve", + "start": "node src/server.js", + "serve": "nwb serve", "precache": "sw-precache --root=public --config=sw-precache-config.json" }, + "engines": { + "node": "5.9.1" + }, + "main": "src/server.js", "dependencies": { + "ejs": "^2.4.1", "events": "1.1.0", + "express": "^4.13.4", "firebase": "2.4.2", "firetruck.js": "0.1.1", "history": "2.1.1", @@ -26,7 +33,8 @@ "react-timeago": "3.0.0", "reactfire": "0.7.0", "scroll-behavior": "0.5.0", - "setimmediate": "1.0.4" + "setimmediate": "1.0.4", + "url-parse": "^1.1.1" }, "devDependencies": { "eslint-config-jonnybuchanan": "2.0.3", diff --git a/public/index.html b/public/index-static.html similarity index 100% rename from public/index.html rename to public/index-static.html diff --git a/src/App.js b/src/App.js index af407da..035e80d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,3 @@ -/* global __VERSION__ */ var React = require('react') var Link = require('react-router/lib/Link') @@ -19,10 +18,12 @@ var App = React.createClass({ SettingsStore.load() StoryStore.loadSession() UpdatesStore.loadSession() + if (typeof window === 'undefined') return window.addEventListener('beforeunload', this.handleBeforeUnload) }, componentWillUnmount() { + if (typeof window === 'undefined') return window.removeEventListener('beforeunload', this.handleBeforeUnload) }, @@ -60,7 +61,6 @@ var App = React.createClass({ {this.props.children}
- {`react-hn v${__VERSION__} | `} insin/react-hn
diff --git a/src/Stories.js b/src/Stories.js index 8dc8fda..c60d523 100644 --- a/src/Stories.js +++ b/src/Stories.js @@ -35,7 +35,7 @@ var Stories = React.createClass({ } }, - componentWillMount() { + componentDidMount() { setTitle(this.props.title) this.store = new StoryStore(this.props.type) this.store.addListener('update', this.handleUpdate) diff --git a/src/mixins/ItemMixin.js b/src/mixins/ItemMixin.js index a4fcd6b..6712ade 100644 --- a/src/mixins/ItemMixin.js +++ b/src/mixins/ItemMixin.js @@ -4,18 +4,16 @@ var TimeAgo = require('react-timeago').default var SettingsStore = require('../stores/SettingsStore') var pluralise = require('../utils/pluralise') +var urlParse = require('url-parse') -var parseHost = (function() { - var a = document.createElement('a') - return function(url) { - a.href = url - var parts = a.hostname.split('.').slice(-3) - if (parts[0] === 'www') { - parts.shift() - } - return parts.join('.') +var parseHost = function(url) { + var hostname = (urlParse(url, true)).hostname + var parts = hostname.split('.').slice(-3) + if (parts[0] === 'www') { + parts.shift() } -})() + return parts.join('.') +} /** * Reusable logic for displaying an item. diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..7a775d5 --- /dev/null +++ b/src/server.js @@ -0,0 +1,44 @@ +var express = require('express') +var React = require('react') +var renderToString = require('react-dom/server').renderToString +var ReactRouter = require('react-router') + +require('babel/register') +var routes = require('./routes') + +var app = express() +app.set('view engine', 'ejs') +app.set('views', process.cwd() + '/src/views') +app.set('port', (process.env.PORT || 5000)) +app.use(express.static('public')) + +app.get('*', function(req, res) { + ReactRouter.match({ + routes: routes, + location: req.url + }, function(err, redirectLocation, props) { + if (err) { + res.status(500).send(err.message) + } + else if (redirectLocation) { + res.redirect(302, redirectLocation.pathname + redirectLocation.search) + } + else if (props) { + var markup = renderToString( + React.createElement(ReactRouter.RouterContext, props, null) + ) + res.render('index', { markup: markup }) + } + else { + res.sendStatus(404) + } + }) +}) + +app.listen(app.get('port'), function(err) { + if (err) { + console.log(err) + return + } + console.log('Running app at localhost:' + app.get('port')) +}) diff --git a/src/services/HNService.js b/src/services/HNService.js index c8e7124..6b9b626 100644 --- a/src/services/HNService.js +++ b/src/services/HNService.js @@ -1,7 +1,6 @@ -var Firetruck = require('firetruck.js') +var Firebase = require('firebase') -var api = new Firetruck('https://hacker-news.firebaseio.com/v0') -api.restore() +var api = new Firebase('https://hacker-news.firebaseio.com/v0') function fetchItem(id, cb) { itemRef(id).once('value', function(snapshot) { diff --git a/src/stores/StoryStore.js b/src/stores/StoryStore.js index 34057df..9400a69 100644 --- a/src/stores/StoryStore.js +++ b/src/stores/StoryStore.js @@ -99,6 +99,7 @@ class StoryStore extends EventEmitter { start() { firebaseRef = HNService.storiesRef(this.type) firebaseRef.on('value', this.onStoriesUpdated) + if (typeof window === 'undefined') return window.addEventListener('storage', this.onStorage) } @@ -107,6 +108,7 @@ class StoryStore extends EventEmitter { firebaseRef.off() firebaseRef = null } + if (typeof window === 'undefined') return window.removeEventListener('storage', this.onStorage) } } @@ -124,6 +126,7 @@ extend(StoryStore, { * Deserialise caches from sessionStorage. */ loadSession() { + if (typeof window === 'undefined') return idCache = parseJSON(window.sessionStorage.idCache, {}) itemCache = parseJSON(window.sessionStorage.itemCache, {}) }, @@ -132,6 +135,7 @@ extend(StoryStore, { * Serialise caches to sessionStorage as JSON. */ saveSession() { + if (typeof window === 'undefined') return window.sessionStorage.idCache = JSON.stringify(idCache) window.sessionStorage.itemCache = JSON.stringify(itemCache) } diff --git a/src/stores/UpdatesStore.js b/src/stores/UpdatesStore.js index ff711e6..9e6029a 100644 --- a/src/stores/UpdatesStore.js +++ b/src/stores/UpdatesStore.js @@ -100,12 +100,14 @@ function handleUpdateItems(items) { var UpdatesStore = extend(new EventEmitter(), { loadSession() { + if (typeof window === 'undefined') return var json = window.sessionStorage.updates updatesCache = (json ? JSON.parse(json) : {comments: {}, stories: {}}) populateUpdates() }, saveSession() { + if (typeof window === 'undefined') return window.sessionStorage.updates = JSON.stringify(updatesCache) }, diff --git a/src/utils/setTitle.js b/src/utils/setTitle.js index e432fe5..34e2faf 100644 --- a/src/utils/setTitle.js +++ b/src/utils/setTitle.js @@ -1,6 +1,7 @@ var {SITE_TITLE} = require('./constants') function setTitle(title) { + if (typeof document === 'undefined') return document.title = (title ? title + ' | ' + SITE_TITLE : SITE_TITLE) } diff --git a/src/utils/storage.js b/src/utils/storage.js index f64187c..d077b8b 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -1,9 +1,14 @@ module.exports = { get(key, defaultValue) { - var value = window.localStorage[key] - return (typeof value != 'undefined' ? value : defaultValue) + if (typeof window !== 'undefined') { + var value = window.localStorage[key] + return (typeof value != 'undefined' ? value : defaultValue) + } }, set(key, value) { - window.localStorage[key] = value + if (typeof window !== 'undefined') { + window.localStorage[key] = value + } } } + diff --git a/src/views/index.ejs b/src/views/index.ejs new file mode 100644 index 0000000..202afd3 --- /dev/null +++ b/src/views/index.ejs @@ -0,0 +1,70 @@ + + + + + + React HN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
<%- markup %>
+ + + + + \ No newline at end of file