From 41176aaee191a110ca175bf350496f928bbfbf41 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Thu, 3 Sep 2015 22:26:56 -0700 Subject: [PATCH 1/2] Extend build process to support multiple views --- Makefile | 16 +++++------ README.md | 9 +++++- package.json | 13 +++++++-- server/defaults.json | 4 +++ server/handler.js | 35 +++++++++++++++++++++++ server/index.js | 29 +++++++++++++++++++ server/log.js | 14 +++++++++ server/routes.json | 11 +++++++ static/index.html => server/template.html | 6 ++-- src/views/about/about.jsx | 15 ++++++++++ src/views/about/about.scss | 3 ++ webpack.config.js | 15 +++++++--- 12 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 server/defaults.json create mode 100644 server/handler.js create mode 100644 server/index.js create mode 100644 server/log.js create mode 100644 server/routes.json rename static/index.html => server/template.html (67%) create mode 100644 src/views/about/about.jsx create mode 100644 src/views/about/about.scss diff --git a/Makefile b/Makefile index 22499ed69..6ed6a1a81 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ ESLINT=./node_modules/.bin/eslint -LIVE=./node_modules/.bin/live-server +NODE=node WATCH=./node_modules/.bin/watch WEBPACK=./node_modules/.bin/webpack @@ -11,7 +11,7 @@ build: @make webpack clean: - rm -rf ./build/*.* + rm -rf ./build mkdir -p build static: @@ -27,6 +27,7 @@ test: lint: $(ESLINT) ./*.js + $(ESLINT) ./server/*.js $(ESLINT) ./src/*.jsx $(ESLINT) ./src/mixins/*.jsx $(ESLINT) ./src/views/**/*.jsx @@ -34,15 +35,14 @@ lint: # ------------------------------------ -start: - @make watch - $(LIVE) ./build --port=8888 --wait=200 --no-browser - watch: + $(WATCH) "make clean && make static" ./static & $(WEBPACK) -d --watch & - $(WATCH) "make static" ./static & wait +start: + node ./server/index.js + # ------------------------------------ -.PHONY: build clean static webpack test lint start watch +.PHONY: build clean static webpack test lint watch start diff --git a/README.md b/README.md index 99b875934..5d33c7983 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,17 @@ ### To Build ```bash npm install +npm run build +``` + +During development, you can use `npm run watch` to cause any update you make to files in either `./static` or `./src` to trigger a rebuild of the project. + +### To Run +```bash npm start ``` -Once running, open `http://localhost:8888` in your browser. Any update you make to files in either `./static` or `./src` should trigger a rebuild of the project and cause your browser to refresh to reflect those changes. +Once running, open `http://localhost:8888` in your browser. If you wish to have the server reload automatically, you can install either [nodemon](https://github.com/remy/nodemon) or [forever](https://github.com/foreverjs/forever). ### To Test ```bash diff --git a/package.json b/package.json index ba4a936f9..0259a3beb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "main": "index.js", "scripts": { "start": "make start", - "test": "make test" + "test": "make test", + "watch": "make watch", + "build": "make build" }, "repository": { "type": "git", @@ -17,14 +19,19 @@ "url": "https://github.com/llk/scratch-www/issues" }, "homepage": "https://github.com/llk/scratch-www#readme", - "dependencies": {}, + "dependencies": { + "bunyan": "1.4.0", + "compression": "1.5.2", + "express": "4.13.3", + "lodash.defaults": "3.1.2", + "mustache": "2.1.3" + }, "devDependencies": { "css-loader": "0.17.0", "eslint": "1.3.1", "eslint-plugin-react": "3.3.1", "json-loader": "0.5.2", "jsx-loader": "0.13.2", - "live-server": "0.8.1", "node-sass": "3.3.2", "react": "0.13.3", "sass-loader": "2.0.1", diff --git a/server/defaults.json b/server/defaults.json new file mode 100644 index 000000000..c94cc1e24 --- /dev/null +++ b/server/defaults.json @@ -0,0 +1,4 @@ +{ + "title": "Scratch - Imagine, Program, Share", + "description": "Scratch is a free programming language and online community where you can create your own interactive stories, games, and animations." +} diff --git a/server/handler.js b/server/handler.js new file mode 100644 index 000000000..cc3134385 --- /dev/null +++ b/server/handler.js @@ -0,0 +1,35 @@ +var crypto = require('crypto'); +var defaults = require('lodash.defaults'); +var fs = require('fs'); +var mustache = require('mustache'); +var path = require('path'); + +/** + * Constructor + */ +function Handler (route) { + // Route definition + defaults(route, require('./defaults.json')); + + // Render template + var location = path.resolve(__dirname, './template.html'); + var template = fs.readFileSync(location, 'utf8'); + var output = mustache.render(template, route); + var checksum = crypto.createHash('md5').update(output).digest('hex'); + + return function (req, res) { + res.set({ + 'Content-Type': 'text/html', + 'Cache-Control': 'public, max-age=31536000', + 'Etag': 'W/"' + checksum + '"' + }); + res.send(output); + }; +} + +/** + * Export a new instance + */ +module.exports = function (route) { + return new Handler(route); +}; diff --git a/server/index.js b/server/index.js new file mode 100644 index 000000000..b12576a5d --- /dev/null +++ b/server/index.js @@ -0,0 +1,29 @@ +var compression = require('compression'); +var express = require('express'); +var path = require('path'); + +var handler = require('./handler'); +var log = require('./log'); +var routes = require('./routes.json'); + +// Server setup +var app = express(); +app.disable('x-powered-by'); +app.use(log()); +app.use(compression()); +app.use(express.static(path.resolve(__dirname, '../build'), { + lastModified: true, + maxAge: '1y' +})); + +// Bind routes +for (var item in routes) { + var route = routes[item]; + app.get(route.pattern, handler(route)); +} + +// Start listening +var port = process.env.PORT || 8888; +app.listen(port, function () { + process.stdout.write('Server listening on port ' + port + '\n'); +}); diff --git a/server/log.js b/server/log.js new file mode 100644 index 000000000..7b9d07064 --- /dev/null +++ b/server/log.js @@ -0,0 +1,14 @@ +var bunyan = require('bunyan'); + +module.exports = function () { + var logger = bunyan.createLogger({ + name: 'www', + serializers: {req: bunyan.stdSerializers.req} + }); + + return function (req, res, next) { + req.log = logger; + req.log.info({req: req}); + next(); + }; +}; diff --git a/server/routes.json b/server/routes.json new file mode 100644 index 000000000..9f58a3e9f --- /dev/null +++ b/server/routes.json @@ -0,0 +1,11 @@ +[ + { + "pattern": "/", + "view": "splash" + }, + { + "pattern": "/about", + "view": "about", + "title": "About" + } +] diff --git a/static/index.html b/server/template.html similarity index 67% rename from static/index.html rename to server/template.html index bb2478330..b6ce4c15f 100644 --- a/static/index.html +++ b/server/template.html @@ -6,8 +6,8 @@ - Scratch - + Scratch - {{title}} + @@ -20,6 +20,6 @@ - + diff --git a/src/views/about/about.jsx b/src/views/about/about.jsx new file mode 100644 index 000000000..9817d8a95 --- /dev/null +++ b/src/views/about/about.jsx @@ -0,0 +1,15 @@ +var React = require('react'); + +require('./about.scss'); + +var View = React.createClass({ + render: function () { + return ( +
+

I am the about page!

+
+ ); + } +}); + +React.render(, document.getElementById('view')); diff --git a/src/views/about/about.scss b/src/views/about/about.scss new file mode 100644 index 000000000..d40c56fb9 --- /dev/null +++ b/src/views/about/about.scss @@ -0,0 +1,3 @@ +#view { + +} diff --git a/webpack.config.js b/webpack.config.js index 3087402d9..bbc477df7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,11 +1,18 @@ var path = require('path'); var webpack = require('webpack'); +var routes = require('./server/routes.json'); +// Prepare all entry points +var entry = { + main: './src/main.jsx' +}; +routes.forEach(function (route) { + entry[route.view] = './src/views/' + route.view + '/' + route.view + '.jsx'; +}); + +// Config module.exports = { - entry: { - main: './src/main.jsx', - splash: './src/views/splash/splash.jsx' - }, + entry: entry, devtool: 'source-map', externals: { 'react': 'React', From 3716d0462c8e368725b4f21a9a98a862253ac334 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Fri, 4 Sep 2015 08:26:18 -0700 Subject: [PATCH 2/2] Merge and resolve feedback from review --- Makefile | 2 +- package.json | 1 + server/template.html | 19 +++- src/components/footer/footer.jsx | 51 ++++++++- src/components/footer/footer.scss | 42 +++++++- src/components/navigation/navigation.jsx | 24 ++++- src/components/navigation/navigation.scss | 120 +++++++++++++++++++++- src/components/news/news.scss | 2 +- src/main.jsx | 2 + src/main.sass | 0 src/main.scss | 50 +++++++++ src/mixins/session.jsx | 10 ++ src/views/splash/splash.jsx | 15 ++- src/views/splash/splash.scss | 2 +- webpack.config.js | 2 +- 15 files changed, 327 insertions(+), 15 deletions(-) delete mode 100644 src/main.sass create mode 100644 src/main.scss create mode 100644 src/mixins/session.jsx diff --git a/Makefile b/Makefile index 6ed6a1a81..1e200a439 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ watch: wait start: - node ./server/index.js + $(NODE) ./server/index.js # ------------------------------------ diff --git a/package.json b/package.json index 0259a3beb..715caa635 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "mustache": "2.1.3" }, "devDependencies": { + "autoprefixer-loader": "2.1.0", "css-loader": "0.17.0", "eslint": "1.3.1", "eslint-plugin-react": "3.3.1", diff --git a/server/template.html b/server/template.html index b6ce4c15f..327a3f373 100644 --- a/server/template.html +++ b/server/template.html @@ -1,5 +1,7 @@ - + + + @@ -9,7 +11,18 @@ Scratch - {{title}} + + + + + + + + + + + @@ -21,5 +34,9 @@ + + + + diff --git a/src/components/footer/footer.jsx b/src/components/footer/footer.jsx index 5b5b3ddec..27b998905 100644 --- a/src/components/footer/footer.jsx +++ b/src/components/footer/footer.jsx @@ -5,8 +5,55 @@ require('./footer.scss'); module.exports = React.createClass({ render: function () { return ( -
-

Footer

+
+ + +
+

Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab

+
); } diff --git a/src/components/footer/footer.scss b/src/components/footer/footer.scss index afee1ebb2..059b2b08d 100644 --- a/src/components/footer/footer.scss +++ b/src/components/footer/footer.scss @@ -1,3 +1,43 @@ #footer { - background-color: yellow; + display: block; + padding: 10px 0; + color: #666; + background-color: #ececec; + + .lists { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + text-align: center; + + dl { + display: inline-block; + width: 130pt; + font-size: 0.85rem; + text-align: left; + vertical-align: top; + } + + dt { + display: block; + margin-bottom: 8px; + font-weight: bold; + } + + dd { + display: block; + margin: 5px 0; + } + } + + .copyright { + display: block; + width: 100%; + text-align: center; + + p { + font-size: 0.7rem; + } + } } diff --git a/src/components/navigation/navigation.jsx b/src/components/navigation/navigation.jsx index bf76244b0..57bfc6b55 100644 --- a/src/components/navigation/navigation.jsx +++ b/src/components/navigation/navigation.jsx @@ -5,8 +5,28 @@ require('./navigation.scss'); module.exports = React.createClass({ render: function () { return ( -
-

Navigation

+
+
); } diff --git a/src/components/navigation/navigation.scss b/src/components/navigation/navigation.scss index 50658be1e..a51e8f819 100644 --- a/src/components/navigation/navigation.scss +++ b/src/components/navigation/navigation.scss @@ -1,3 +1,121 @@ #navigation { - background-color: cyan; + position: fixed; + display: block; + top: 0; + left: 0; + width: 100%; + height: 35px; + + background-color: #0f8bc0; + + ul { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + + height: 35px; + margin: 0; + padding: 0; + list-style: none; + + li { + display: inline-block; + align-self: flex-start; + float: left; + height: 100%; + border-left: 1px solid rgb(20, 154, 203); + } + + .logo { + border-left: none; + + a { + display: block; + width: 80px; + height: 35px; + margin: 4px 6px 0 0; + + border: none; + background-image: url('/images/logo_sm.png'); + background-repeat: no-repeat; + + &:hover { + background-image: url('/images/logo_sm_highlight.png'); + } + } + } + + .link { + a { + display: block; + height: 28px; + padding: 7px 15px 0 15px; + + color: white; + text-decoration: none; + font-size: 0.95rem; + white-space: nowrap; + } + + a:hover { + background-color: rgb(1, 96, 135); + } + } + + .search { + flex-grow: 3; + border-right: none; + + form { + margin: 6px 0 0 15px; + } + + input { + display: inline-block; + height: 20px; + outline: none; + border: none; + } + + input[type=submit] { + position: absolute; + width: 30px; + height: 22px; + + background-color: white; + background-image: url(/images/nav-search-glass.png); + background-repeat: no-repeat; + background-position: center center; + border-right: 1px solid #efefef; + border-radius: 10px 0 0 10px; + } + + input[type=text] { + width: calc(100% - 50px); + padding-left: 36px; + border-radius: 10px; + font-size: 0.85em; + } + + .ie9 input[type=text] { + width: 70px; + } + } + + .right { + align-self: flex-end; + float: right; + margin-left: auto; + font-weight: bold; + + a:hover { + background-color: #f79231; + } + + &:last-child { + border-right: 1px solid rgb(20, 154, 203); + } + } + } } diff --git a/src/components/news/news.scss b/src/components/news/news.scss index 6270cc9be..77f858ca6 100644 --- a/src/components/news/news.scss +++ b/src/components/news/news.scss @@ -1,3 +1,3 @@ .news { - background-color: white; + } diff --git a/src/main.jsx b/src/main.jsx index b4ae23fa5..9845b0ad1 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,5 +1,7 @@ var React = require('react'); +require('./main.scss'); + var Navigation = require('./components/navigation/navigation.jsx'); var Footer = require('./components/footer/footer.jsx'); diff --git a/src/main.sass b/src/main.sass deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main.scss b/src/main.scss new file mode 100644 index 000000000..ea9b94e54 --- /dev/null +++ b/src/main.scss @@ -0,0 +1,50 @@ +/* Tags */ +html, body { + display: block; + margin: 0; + padding: 0; + + color: #322f31; + background-color: #fdfdfd; + + font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; +} + +/* Typography */ +h1, h2, h3, h4 { + margin: 0; + padding: 0; + border: 0; + + color: #554747; + font-weight: 700; +} + +h4 { + font-size: 1.0rem; + line-height: 1.1rem; +} + +/* Links */ +a:link, a:visited, a:active { + color: #1aa0d8; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Classes */ +.inner { + width: 942px; + margin: 0 auto; +} + +#view { + min-height: 768px; + padding: 10px 0; + + /* Compensate for fixed navigation */ + margin-top: 35px; +} diff --git a/src/mixins/session.jsx b/src/mixins/session.jsx new file mode 100644 index 000000000..8df039b89 --- /dev/null +++ b/src/mixins/session.jsx @@ -0,0 +1,10 @@ +module.exports = { + getInitialState: function () { + return { + session: {} + }; + }, + componentWillMount: function () { + // @todo Fetch session from API + } +}; diff --git a/src/views/splash/splash.jsx b/src/views/splash/splash.jsx index 9fdf49e03..7c9719004 100644 --- a/src/views/splash/splash.jsx +++ b/src/views/splash/splash.jsx @@ -1,17 +1,22 @@ var React = require('react'); var Api = require('../../mixins/api.jsx'); +var Session = require('../../mixins/session.jsx'); + var News = require('../../components/news/news.jsx'); require('./splash.scss'); var View = React.createClass({ mixins: [ - Api + Api, + Session ], getInitialState: function () { return { - news: [] + activity: [], + news: [], + featured: [] }; }, componentDidMount: function () { @@ -21,9 +26,11 @@ var View = React.createClass({ }, render: function () { return ( -
-

I am the splash page!

+
+
+
+
); } diff --git a/src/views/splash/splash.scss b/src/views/splash/splash.scss index a2915c31f..d40c56fb9 100644 --- a/src/views/splash/splash.scss +++ b/src/views/splash/splash.scss @@ -1,3 +1,3 @@ #view { - background-color: red; + } diff --git a/webpack.config.js b/webpack.config.js index bbc477df7..791c7e2d7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,7 +36,7 @@ module.exports = { }, { test: /\.scss$/, - loader: 'style!css!sass' + loader: 'style!css!sass!autoprefixer-loader?browsers=last 3 versions' } ] },