├── .gitignore ├── Dockerfile ├── Gruntfile.js ├── README.md ├── assets ├── fonts │ ├── MaterialIcons-Regular.ttf │ ├── MaterialIcons-Regular.woff │ └── MaterialIcons-Regular.woff2 └── images │ ├── charlixcx-sucker-cd.jpg │ ├── ipad-mini-4.jpg │ └── white-tshirt.jpg ├── package.json ├── sass ├── _alerts.scss ├── _bootstrap.scss ├── _cart.scss ├── _flex-utilities.scss ├── _frame.scss ├── _helpers.scss ├── _material-icons.scss ├── _mixins.scss ├── _modals.scss ├── _navbar.scss ├── _spinners.scss ├── _standardise.scss ├── _transitions.scss ├── _type.scss ├── _variables.scss ├── checkoutPage.scss ├── common.scss ├── productPage.scss └── shopPage.scss ├── server.js ├── typescript ├── .baseDir.ts ├── client │ ├── api │ │ ├── shop.ts │ │ └── socketManager.ts │ └── app.tsx ├── config.ts ├── server │ ├── Html.tsx │ ├── backend-server.tsx │ ├── polyfills.ts │ ├── products.json │ └── webpack-server.ts ├── tsconfig.json ├── typings │ ├── Object.d.ts │ ├── classnames.d.ts │ ├── fetch.d.ts │ ├── finput.d.ts │ ├── node.d.ts │ ├── promise.d.ts │ ├── react-router │ │ ├── history.d.ts │ │ ├── react-router-redux.d.ts │ │ └── react-router.d.ts │ ├── react │ │ ├── react-addons-css-transition-group.d.ts │ │ ├── react-addons-transition-group.d.ts │ │ ├── react-dom.d.ts │ │ ├── react-helmet.d.ts │ │ └── react.d.ts │ ├── redux │ │ ├── react-redux.d.ts │ │ ├── redux-async-connect.d.ts │ │ ├── redux-devtools-dock-monitor.d.ts │ │ ├── redux-devtools-log-monitor.d.ts │ │ ├── redux-devtools.d.ts │ │ ├── redux-logger.d.ts │ │ ├── redux-thunk.d.ts │ │ └── redux.d.ts │ ├── socket.io-client.d.ts │ ├── socket.io.d.ts │ └── webpack.d.ts └── universal │ ├── Routes.tsx │ ├── components │ ├── AddFundsForm.tsx │ ├── Alerts │ │ └── Alert.tsx │ ├── Cart │ │ └── Cart.tsx │ ├── CheckoutPage.tsx │ ├── Finput.tsx │ ├── Modal.tsx │ ├── Navbar.tsx │ ├── PageLoader.tsx │ ├── Product │ │ ├── Product.tsx │ │ ├── ProductItem.tsx │ │ ├── ProductListPagination.tsx │ │ └── ProductsList.tsx │ ├── ProductPage.tsx │ └── SpringItem.tsx │ ├── configureStore.tsx │ ├── constants │ ├── AlertTypes.ts │ └── Modals.ts │ ├── containers │ ├── Alerts │ │ └── GlobalAlerts.tsx │ ├── App.tsx │ ├── Cart │ │ ├── Cart.tsx │ │ └── CartProduct.tsx │ ├── CheckoutPage.tsx │ ├── Modals │ │ └── AddFundsModal.tsx │ ├── Navbar.tsx │ ├── Product │ │ └── ProductList.tsx │ ├── ProductPage.tsx │ └── ShopPage.tsx │ ├── interfaces │ ├── Action.ts │ ├── Alert.ts │ ├── AppPage.ts │ ├── AppState.ts │ ├── GlobalState.ts │ ├── Link.ts │ ├── Product.ts │ ├── RoutingState.ts │ └── User.ts │ └── redux │ ├── ReducerRegistry.ts │ ├── core.ts │ └── modules │ ├── alertManager.ts │ ├── cart.ts │ ├── global.ts │ ├── productPage.ts │ ├── products.ts │ └── user.ts └── webpack ├── dev.config.js ├── prod.config.js └── webpack-isomorphic-tools.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | messages 4 | .idea/ 5 | npm-debug.log 6 | .tscache/ 7 | javascript 8 | ts-command-* 9 | webpack-assets.json 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | RUN npm cache clean 7 | RUN npm install 8 | 9 | COPY . ./ 10 | RUN npm run build 11 | 12 | EXPOSE 9999 13 | ENV NODE_ENV=production PORT=9999 14 | 15 | ENTRYPOINT node server.js 16 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.loadNpmTasks('grunt-sass'); 3 | grunt.loadNpmTasks('grunt-contrib-watch'); 4 | grunt.loadNpmTasks('grunt-express-server'); 5 | grunt.loadNpmTasks('grunt-contrib-clean'); 6 | grunt.loadNpmTasks('grunt-contrib-copy'); 7 | grunt.loadNpmTasks('grunt-ts'); 8 | grunt.loadNpmTasks('grunt-webpack'); 9 | grunt.loadNpmTasks('grunt-keepalive'); 10 | grunt.loadNpmTasks('grunt-contrib-watch'); 11 | 12 | var webpack = require("webpack"); 13 | var webpackConfig = require("./webpack/prod.config.js"); 14 | 15 | grunt.initConfig({ 16 | clean: ['build', 'javascript'], 17 | webpack: { 18 | main: webpackConfig 19 | }, 20 | copy: { 21 | main: { 22 | files: [ 23 | { expand: true, src: ['assets/**/*'], dest: 'build/' }, 24 | { expand: true, cwd: 'vendor/react-router', src: ['**/*'], dest: 'node_modules/react-router/' }, 25 | { expand: true, cwd: 'typescript', src: ['**/*.json'], dest: 'javascript/' }, 26 | { expand: true, flatten: true, src: ['node_modules/material-design-icons/iconfont/MaterialIcons-Regular.ttf'], dest: 'assets/fonts/' }, 27 | { expand: true, flatten: true, src: ['node_modules/material-design-icons/iconfont/MaterialIcons-Regular.woff'], dest: 'assets/fonts/' }, 28 | { expand: true, flatten: true, src: ['node_modules/material-design-icons/iconfont/MaterialIcons-Regular.woff2'], dest: 'assets/fonts/' } 29 | ] 30 | } 31 | }, 32 | ts: { 33 | options: { 34 | additionalFlags: '--jsx react', 35 | rootDir: './typescript' 36 | }, 37 | default: { 38 | tsconfig: './typescript/tsconfig.json' 39 | } 40 | }, 41 | sass: { 42 | default: { 43 | options: { 44 | check: true 45 | }, 46 | files: { 47 | 'build/build.css': 'sass/app.scss' 48 | } 49 | } 50 | }, 51 | express: { 52 | dev: { 53 | options: { 54 | node_env: 'development' 55 | } 56 | }, 57 | prod: { 58 | options: { 59 | node_env: 'production' 60 | } 61 | }, 62 | options: { 63 | script: 'server.js', 64 | port: 9999 65 | } 66 | }, 67 | watch: { 68 | options: { 69 | forever: false, 70 | // for grunt-contrib-watch v0.5.0+, 'nospawn: true' for lower versions. 71 | // Without this option specified express won't be reloaded 72 | spawn: false, 73 | livereload: true 74 | }, 75 | assets: { 76 | files: ['assets/**/*', 'typescript/**/*.json'], 77 | tasks: ['copy', 'restart-server'] 78 | }, 79 | express: { 80 | files: ['server.js', 'javascript/server/**/*.js'], 81 | tasks: ['restart-server'] 82 | } 83 | }, 84 | }); 85 | 86 | grunt.registerTask('pre-compile', ['clean', 'ts']); 87 | grunt.registerTask('restart-server', ['express:dev:stop', 'express:dev']); 88 | 89 | grunt.registerTask('build', ['pre-compile', 'copy', 'webpack']); 90 | grunt.registerTask('serve:dev', ['pre-compile', 'copy', 'express:dev', 'watch']); 91 | grunt.registerTask('serve:prod', ['build', 'express:prod', 'keepalive']); 92 | grunt.registerTask("default", ['serve:dev']); 93 | }; 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | See the related blog post [here](http://blog.scottlogic.com/2016/02/05/a-lazy-isomorphic-react-experiment.html) 3 | 4 | [See it running here!](https://shopping-cart.alisd.io/) 5 | 6 | ## Setup 7 | 8 | #### Clone 9 | 10 | `git clone git@github.com:alisd23/lazy-isomorphic-react.git` 11 | 12 | 13 | #### Install dependencies 14 | 15 | `npm install` 16 | 17 | 18 | #### Development 19 | 20 | `grunt serve:dev` 21 | 22 | 23 | #### Production 24 | 25 | `grunt build` 26 | `grunt serve:prod` 27 | 28 | 29 | #### View Application 30 | 31 | Go to `localhost:9999` to see the application 32 | -------------------------------------------------------------------------------- /assets/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/fonts/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/fonts/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /assets/images/charlixcx-sucker-cd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/images/charlixcx-sucker-cd.jpg -------------------------------------------------------------------------------- /assets/images/ipad-mini-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/images/ipad-mini-4.jpg -------------------------------------------------------------------------------- /assets/images/white-tshirt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisd23/lazy-isomorphic-react/f39f7360fb1f20742d43c76b3099fa9ebfb1f9d8/assets/images/white-tshirt.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-multipage-test", 3 | "private": true, 4 | "scripts": { 5 | "build": "grunt build" 6 | }, 7 | "dependencies": { 8 | "bootstrap": "^4.0.0-alpha.2", 9 | "classnames": "^2.2.3", 10 | "finput": "^0.1.1", 11 | "material-design-icons": "^2.1.3", 12 | "react": "^0.14.0", 13 | "react-addons-css-transition-group": "^0.14.7", 14 | "react-addons-transition-group": "^0.14.7", 15 | "react-dom": "^0.14.0", 16 | "react-helmet": "^2.3.1", 17 | "react-redux": "^4.0.0", 18 | "react-router": "^2.0.0-rc5", 19 | "react-router-redux": "^2.1.0", 20 | "rebound": "0.0.13", 21 | "redux": "^3.0.4", 22 | "redux-async-connect": "^0.1.11", 23 | "redux-thunk": "^1.0.0", 24 | "socket.io": "^1.4.5", 25 | "socket.io-client": "^1.4.5", 26 | "webpack-isomorphic-tools": "^2.2.26" 27 | }, 28 | "devDependencies": { 29 | "autoprefixer": "^6.3.1", 30 | "babel": "^6.5.2", 31 | "babel-core": "^6.0.0", 32 | "babel-loader": "^6.1.0", 33 | "babel-plugin-react-transform": "^2.0.0", 34 | "babel-preset-es2015": "^6.5.0", 35 | "babel-preset-react": "^6.5.0", 36 | "clean-webpack-plugin": "^0.1.8", 37 | "copy-webpack-plugin": "^1.1.1", 38 | "express": "^4.13.3", 39 | "extract-text-webpack-plugin": "^1.0.1", 40 | "grunt": "^0.4.5", 41 | "grunt-cli": "^0.1.13", 42 | "grunt-contrib-clean": "^0.7.0", 43 | "grunt-contrib-copy": "^0.8.2", 44 | "grunt-contrib-watch": "^0.6.1", 45 | "grunt-express-server": "^0.5.1", 46 | "grunt-keepalive": "0.0.1", 47 | "grunt-sass": "^1.1.0", 48 | "grunt-ts": "^5.1.0", 49 | "grunt-webpack": "^1.0.11", 50 | "json-loader": "^0.5.3", 51 | "jsx-loader": "^0.13.2", 52 | "style-loader": "^0.13.0", 53 | "url-loader": "^0.5.7", 54 | "css-loader": "^0.23.1", 55 | "file-loader": "^0.8.5", 56 | "postcss-loader": "^0.8.0", 57 | "node-sass": "^3.4.2", 58 | "path": "^0.12.7", 59 | "react-hot-loader": "^1.3.0", 60 | "redux-devtools": "^3.0.1", 61 | "redux-devtools-dock-monitor": "^1.0.1", 62 | "redux-devtools-log-monitor": "^1.0.2", 63 | "redux-logger": "^2.0.1", 64 | "resolve-url-loader": "^1.4.3", 65 | "sass-loader": "^3.1.2", 66 | "tslint": "^3.0.0", 67 | "typescript": "^1.8.0", 68 | "webpack": "^2.1.0-beta.4", 69 | "webpack-dev-server": "^2.0.0-beta" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sass/_alerts.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './variables'; 3 | 4 | .global-alerts { 5 | position: fixed; 6 | z-index: 1000; 7 | top: 120px; left: 10px; 8 | bottom: 0; 9 | pointer-events: none; 10 | 11 | .alert { 12 | position: relative; 13 | cursor: pointer; 14 | pointer-events: all; 15 | width: 400px; 16 | background-color: white; 17 | padding: 1.25rem 1.5rem; 18 | border: 4px solid transparent; 19 | box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.3); 20 | border-top-left-radius: 0; 21 | border-bottom-left-radius: 0; 22 | transition: transform 0.2s ease, opacity 0.2s ease; 23 | 24 | &:hover { 25 | transform: scale(0.95); 26 | } 27 | 28 | &.error { border-color: rgba($red, 0.6); } 29 | &.success { border-color: rgba($green, 0.6); } 30 | &.info { border-color: rgba($blue, 0.6); } 31 | } 32 | } 33 | 34 | 35 | // FROM 36 | .global-alert-enter, 37 | .global-alert-leave.global-alert-leave-active { 38 | left: -100%; 39 | } 40 | 41 | // TO 42 | .global-alert-leave, 43 | .global-alert-enter.global-alert-enter-active { 44 | left: 0; 45 | } 46 | 47 | // TRANSITION 48 | .global-alert-enter.global-alert-enter-active, 49 | .global-alert-leave.global-alert-leave-active { 50 | transition: left 0.3s ease-in-out; 51 | } 52 | -------------------------------------------------------------------------------- /sass/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | 2 | $enable-flex: true; 3 | 4 | // Core variables and mixins 5 | @import "node_modules/bootstrap/scss/variables"; 6 | @import "node_modules/bootstrap/scss/mixins"; 7 | 8 | // Reset and dependencies 9 | @import "node_modules/bootstrap/scss/normalize"; 10 | @import "node_modules/bootstrap/scss/print"; 11 | 12 | // Core CSS 13 | @import "node_modules/bootstrap/scss/reboot"; 14 | @import "node_modules/bootstrap/scss/type"; 15 | @import "node_modules/bootstrap/scss/images"; 16 | @import "node_modules/bootstrap/scss/code"; 17 | @import "node_modules/bootstrap/scss/grid"; 18 | @import "node_modules/bootstrap/scss/tables"; 19 | @import "node_modules/bootstrap/scss/forms"; 20 | @import "node_modules/bootstrap/scss/buttons"; 21 | 22 | // Components 23 | @import "node_modules/bootstrap/scss/dropdown"; 24 | @import "node_modules/bootstrap/scss/input-group"; 25 | @import "node_modules/bootstrap/scss/custom-forms"; 26 | @import "node_modules/bootstrap/scss/nav"; 27 | @import "node_modules/bootstrap/scss/navbar"; 28 | @import "node_modules/bootstrap/scss/labels"; 29 | @import "node_modules/bootstrap/scss/alert"; 30 | 31 | // Components w/ JavaScript 32 | // @import "modal"; 33 | @import "node_modules/bootstrap/scss/tooltip"; 34 | @import "node_modules/bootstrap/scss/popover"; 35 | 36 | // Utility classes 37 | @import "node_modules/bootstrap/scss/utilities"; 38 | @import "node_modules/bootstrap/scss/utilities-background"; 39 | @import "node_modules/bootstrap/scss/utilities-spacing"; 40 | @import "node_modules/bootstrap/scss/utilities-responsive"; 41 | -------------------------------------------------------------------------------- /sass/_cart.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './variables'; 3 | @import './mixins'; 4 | 5 | .add-button { 6 | @include action-button(65px, white, $green); 7 | 8 | box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.2); 9 | padding: 0.5rem; 10 | 11 | &[disabled] { 12 | color: $gray-light; 13 | background-color: $gray-lighter; 14 | } 15 | } 16 | 17 | .remove-button { 18 | @include action-button(30px, white, $red); 19 | 20 | box-shadow: -2px 2px 3px rgba(0, 0, 0, 0.2); 21 | padding: 0.3rem; 22 | } 23 | 24 | .quantity-controls { 25 | display: inline-flex; 26 | vertical-align: middle; 27 | cursor: pointer; 28 | flex-direction: column; 29 | transition: all 0.4s ease; 30 | 31 | i { 32 | color: #bbb; 33 | 34 | &:hover { 35 | color: #999; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sass/_flex-utilities.scss: -------------------------------------------------------------------------------- 1 | 2 | .flex { 3 | display: flex; 4 | } 5 | .flex-wrap { 6 | flex-wrap: wrap; 7 | } 8 | .no-wrap { 9 | flex-wrap: nowrap; 10 | } 11 | .flex-expand { 12 | flex: 1 1 auto; 13 | } 14 | .flex-static { 15 | flex: 0 0 auto; 16 | } 17 | .column-center { 18 | justify-content: center; 19 | } 20 | .row-center { 21 | align-items: center; 22 | } 23 | .column { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | .center-a { 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | } 32 | -------------------------------------------------------------------------------- /sass/_frame.scss: -------------------------------------------------------------------------------- 1 | 2 | .frame { 3 | display: inline-block; 4 | width: 150px; 5 | height: 150px; 6 | 7 | img { 8 | max-width: 100%; 9 | max-height: 100%; 10 | object-fit: contain; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sass/_helpers.scss: -------------------------------------------------------------------------------- 1 | 2 | .contain { 3 | position: relative; 4 | } 5 | 6 | .cover { 7 | position: absolute; 8 | top: 0; bottom: 0; 9 | left: 0; right: 0; 10 | } 11 | -------------------------------------------------------------------------------- /sass/_material-icons.scss: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: 'Material Icons'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url("../assets/fonts/MaterialIcons-Regular.woff2") format('woff2'), 7 | url("../assets/fonts/MaterialIcons-Regular.woff") format('woff'), 8 | url("../assets/fonts/MaterialIcons-Regular.ttf") format('truetype'); 9 | } 10 | 11 | .material-icons { 12 | font-family: 'Material Icons'; 13 | font-weight: normal; 14 | font-style: normal; 15 | font-size: 24px; /* Preferred icon size */ 16 | display: inline-block; 17 | width: 1em; 18 | height: 1em; 19 | line-height: 1; 20 | text-transform: none; 21 | letter-spacing: normal; 22 | word-wrap: normal; 23 | white-space: nowrap; 24 | direction: ltr; 25 | 26 | /* Support for all WebKit browsers. */ 27 | -webkit-font-smoothing: antialiased; 28 | /* Support for Safari and Chrome. */ 29 | text-rendering: optimizeLegibility; 30 | 31 | /* Support for Firefox. */ 32 | -moz-osx-font-smoothing: grayscale; 33 | 34 | /* Support for IE. */ 35 | font-feature-settings: 'liga'; 36 | } 37 | 38 | // Rules for sizing the icon. 39 | .material-icons { 40 | &.md-14 { font-size: 14px; } 41 | &.md-18 { font-size: 18px; } 42 | &.md-24 { font-size: 24px; } 43 | &.md-36 { font-size: 36px; } 44 | &.md-48 { font-size: 48px; } 45 | } 46 | 47 | // Rules for using icons as black on a light background. 48 | .material-icons { 49 | &.md-dark { color: rgba(0, 0, 0, 0.54); } 50 | &.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); } 51 | &.md-light { color: rgba(255, 255, 255, 1); } 52 | &.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); } 53 | } 54 | 55 | .material-icons { 56 | &.light { color: #dddddd; } 57 | &.star-gold { color: gold; } 58 | } 59 | -------------------------------------------------------------------------------- /sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin action-button($size: 50px, $color: white, $bg: $gray-light) { 3 | width: $size; 4 | height: $size; 5 | border-radius: 50%; 6 | 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | background-color: $bg; 12 | color: $color; 13 | 14 | cursor: pointer; 15 | transition: all 0.3s ease; 16 | text-align: center; 17 | 18 | &:hover { 19 | background-color: darken($bg, 10%); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sass/_modals.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './helpers'; 3 | @import './variables'; 4 | 5 | .modal { 6 | position: fixed; 7 | display: flex; 8 | align-items: flex-start; 9 | justify-content: center; 10 | top: 0; bottom: 0; 11 | left: 0; right: 0; 12 | 13 | .modal-backdrop { 14 | @extend .cover; 15 | background-color: rgba(0, 0, 0, 0.4); 16 | } 17 | 18 | .modal-content { 19 | position: relative; 20 | margin-top: 100px; 21 | width: 600px; 22 | background-color: white; 23 | z-index: $zindex-modal; 24 | padding: 4rem; 25 | border-radius: 5px; 26 | box-shadow: -5px 6px 10px rgba(0, 0, 0, 0.3); 27 | } 28 | 29 | .modal-header { 30 | text-align: center; 31 | } 32 | .modal-body { 33 | margin-top: 3rem; 34 | } 35 | 36 | .close { 37 | position: absolute; 38 | top: 1rem; right: 1rem; 39 | color: #ccc; 40 | padding: 0.2rem; 41 | cursor: pointer; 42 | transition: all 0.4s ease; 43 | 44 | &:hover { 45 | color: #aaa; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sass/_navbar.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './variables'; 3 | @import './mixins'; 4 | 5 | .navbar-brand { 6 | i { 7 | vertical-align: text-bottom; 8 | } 9 | } 10 | 11 | .add-funds { 12 | @include action-button(24px, white, $blue); 13 | 14 | box-shadow: -2px 2px 3px rgba(0, 0, 0, 0.2); 15 | padding: 0.3rem; 16 | } 17 | -------------------------------------------------------------------------------- /sass/_spinners.scss: -------------------------------------------------------------------------------- 1 | // Loading screen spinner - full screen with grey backdrop 2 | .loading-screen { 3 | z-index: 10; 4 | display: none; 5 | position: absolute; 6 | top: 0; left: 0; 7 | bottom: 0; right: 0; 8 | border-radius: inherit; 9 | display: flex; 10 | 11 | &.in { 12 | display: block; 13 | } 14 | 15 | &.backdrop { 16 | background: rgba(white, 0.6); 17 | } 18 | } 19 | 20 | @keyframes Spinner { 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | .spinner { 26 | @extend .row-xs-center; 27 | 28 | &:before { 29 | content: 'Loading…'; 30 | width: 45px; 31 | height: 45px; 32 | display: block; 33 | } 34 | &.spinner-sm:before { 35 | width: 20px; 36 | height: 20px; 37 | } 38 | } 39 | 40 | .spinner:not(:required) { 41 | &:before { 42 | content: ''; 43 | border-radius: 50%; 44 | border: 5px solid rgba(#aaa, 0.4); 45 | border-top-color: rgba(#fff, 0.8); 46 | animation: Spinner 0.75s linear infinite; 47 | -webkit-animation: Spinner 0.75s linear infinite; 48 | } 49 | &.spinner-sm:before { 50 | border-width: 3px; 51 | } 52 | &.spinner-light:before { 53 | border-color: rgba(#eee, 0.4); 54 | border-top-color: rgba(#fff, 0.8); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sass/_standardise.scss: -------------------------------------------------------------------------------- 1 | 2 | a { 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /sass/_transitions.scss: -------------------------------------------------------------------------------- 1 | 2 | // MODAL transitions 3 | .modal-enter, .modal-leave.modal-leave-active { 4 | opacity: 0; 5 | 6 | .modal-content { 7 | transform: scale(0.7); 8 | } 9 | } 10 | 11 | .modal-leave, .modal-enter.modal-enter-active { 12 | opacity: 1; 13 | 14 | .modal-content { 15 | transform: scale(1); 16 | } 17 | } 18 | 19 | .modal-enter.modal-enter-active, .modal-leave.modal-leave-active { 20 | transition: opacity 0.5s ease; 21 | 22 | .modal-content { 23 | transition: opacity 0.5s ease, transform 0.4s ease-out; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sass/_type.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700); 2 | 3 | body, html { 4 | font-family: 'Open sans'; 5 | } 6 | 7 | .small-caps { 8 | text-transform: uppercase; 9 | letter-spacing: 2px; 10 | } 11 | 12 | .strong { 13 | font-weight: bold; 14 | } 15 | -------------------------------------------------------------------------------- /sass/_variables.scss: -------------------------------------------------------------------------------- 1 | @import '../node_modules/bootstrap/scss/variables'; 2 | 3 | $green: #5CB85C; 4 | $red: #9B2121; 5 | $blue: #0275d8; 6 | -------------------------------------------------------------------------------- /sass/checkoutPage.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './cart'; 3 | 4 | #checkout-page { 5 | padding-top: 2rem; 6 | max-width: 600px; 7 | margin: 0 auto; 8 | } 9 | -------------------------------------------------------------------------------- /sass/common.scss: -------------------------------------------------------------------------------- 1 | @import './bootstrap'; 2 | @import './variables'; 3 | @import './flex-utilities'; 4 | @import './standardise'; 5 | @import './material-icons'; 6 | @import './navbar'; 7 | @import './frame'; 8 | @import './type'; 9 | @import './alerts'; 10 | @import './helpers'; 11 | @import './spinners'; 12 | @import './transitions'; 13 | @import './modals'; 14 | 15 | #main-container { 16 | position: relative; 17 | overflow-y: auto; 18 | } 19 | 20 | #page-loader { 21 | position: fixed; 22 | top: 0; bottom: 0; 23 | left: 0; right: 0; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | background-color: rgba(white, 0.6); 29 | pointer-events: none; 30 | } 31 | -------------------------------------------------------------------------------- /sass/productPage.scss: -------------------------------------------------------------------------------- 1 | 2 | #product-page { 3 | padding-top: 2rem; 4 | 5 | .star { 6 | cursor: pointer; 7 | } 8 | .add-to-cart { 9 | width: 300px; 10 | } 11 | } 12 | 13 | .test { 14 | background-color: blue; 15 | } 16 | -------------------------------------------------------------------------------- /sass/shopPage.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './variables'; 3 | @import './cart'; 4 | 5 | 6 | .checkout { 7 | width: 450px; 8 | max-width: 100%; 9 | } 10 | 11 | .cart-list { 12 | min-width: 300px; 13 | } 14 | 15 | .pagination { 16 | display: flex; 17 | justify-content: center; 18 | 19 | .pagination-link { 20 | padding: 0.2rem 0.6rem; 21 | font-size: $font-size-lg; 22 | cursor: pointer; 23 | 24 | &.active span { 25 | color: $blue; 26 | border-bottom: 2px solid $blue; 27 | } 28 | 29 | i { 30 | vertical-align: middle; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | const webPackServer = require('./javascript/server/webpack-server').default; 3 | const apiServer = require('./javascript/server/backend-server').default; 4 | 5 | const PORT = process.env.PORT || 9000; 6 | const PROD = process.env.NODE_ENV === 'production'; 7 | 8 | /** 9 | * Define isomorphic constants. 10 | */ 11 | global.__CLIENT__ = false; 12 | global.__SERVER__ = true; 13 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 14 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 15 | 16 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 17 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('./webpack/webpack-isomorphic-tools')) 18 | .development(__DEVELOPMENT__) 19 | .server(__dirname, function() { 20 | 21 | if (PROD) { 22 | apiServer(PORT); 23 | } else { 24 | apiServer(PORT - 1); 25 | webPackServer(PORT); 26 | } 27 | 28 | }); 29 | 30 | // webPackServer(PORT); 31 | -------------------------------------------------------------------------------- /typescript/.baseDir.ts: -------------------------------------------------------------------------------- 1 | // Ignore this file. See https://github.com/grunt-ts/grunt-ts/issues/77 -------------------------------------------------------------------------------- /typescript/client/api/shop.ts: -------------------------------------------------------------------------------- 1 | import IProduct from '../../universal/interfaces/Product'; 2 | 3 | /** 4 | * Mocking client-server processing 5 | */ 6 | 7 | const TIMEOUT = 100; 8 | 9 | interface buyProductsPayload { 10 | products: number, 11 | total: number, 12 | balance: number 13 | } 14 | 15 | 16 | export default { 17 | 18 | // Can handle timeouts here (Simulate loading delay for now) 19 | getProducts(callback: Function, timeout?: number) { 20 | fetch('/products') 21 | .then((response) => response.json()) 22 | .then((products) => (callback(products), console.log(products))) 23 | .catch((err) => console.debug(err)); 24 | }, 25 | 26 | buyProducts( 27 | cart: IProduct[], 28 | total: number, 29 | balance: number, 30 | successCB: Function, 31 | errorCB: Function, 32 | timeout?: number 33 | ) { 34 | // Simulate 40% chance of error 35 | if (total <= balance) { 36 | setTimeout(() => Math.random() > 0.4 ? successCB() : errorCB(), timeout = TIMEOUT); 37 | } else { 38 | errorCB('Not enough cash money in da bank'); 39 | } 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /typescript/client/api/socketManager.ts: -------------------------------------------------------------------------------- 1 | import * as io from 'socket.io-client'; 2 | import { Store } from 'redux'; 3 | import shop from './shop'; 4 | 5 | import { newProductReceived } from '../../universal/redux/modules/products'; 6 | 7 | export default class SocketManager { 8 | private _socket: SocketIOClient.Socket; 9 | private _store: Store; 10 | 11 | constructor(store: Store) { 12 | this._store = store; 13 | } 14 | 15 | connect() { 16 | this._socket = io(); 17 | 18 | this._socket.on('connect', () => { 19 | this.setupEventListeners(); 20 | }); 21 | 22 | return this._socket; 23 | } 24 | 25 | get socket() { 26 | return this._socket; 27 | } 28 | get store() { 29 | return this._store; 30 | } 31 | 32 | private setupEventListeners() { 33 | this._socket.on('authenticated', (data) => { 34 | console.log('Sockets connected', data); 35 | }) 36 | this._socket.on('new_product', (product) => { 37 | this.store.dispatch(newProductReceived(product)); 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /typescript/client/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import { Store, combineReducers } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import { Router, browserHistory } from 'react-router'; 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | import { createDevTools } from 'redux-devtools'; 10 | 11 | import ReducerRegistry from '../universal/redux/ReducerRegistry'; 12 | 13 | import Routes from '../universal/Routes'; 14 | import { configureClient } from '../universal/configureStore'; 15 | import { match } from 'react-router'; 16 | 17 | import coreReducers from '../universal/redux/core'; 18 | import SocketManager from './api/socketManager'; 19 | 20 | 21 | const DevTools = createDevTools( 22 | 26 | 27 | 28 | ) 29 | 30 | const reducerRegistry = new ReducerRegistry(coreReducers); 31 | const routes = new Routes(reducerRegistry); 32 | 33 | /** 34 | * This magic allows router to load correct reducer and components depending on which route we are in 35 | */ 36 | match({ history: browserHistory, routes: routes.configure() } as any, (error, redirectLocation, renderProps) => { 37 | 38 | const initialState = (window as any).__INITIAL_STATE__; 39 | const store: Store = configureClient(reducerRegistry, DevTools, initialState); 40 | 41 | const sockets = new SocketManager(store); 42 | sockets.connect(); 43 | 44 | routes.injectStore(store); 45 | 46 | render( 47 | 48 |
49 | 50 |
51 |
, 52 | document.getElementById('root') 53 | ); 54 | 55 | // Render dev tools 56 | render( 57 | , 58 | document.getElementById('dev-tools') 59 | ); 60 | 61 | 62 | //--------------------------// 63 | // HOT RELOADING REDUCERS // 64 | //--------------------------// 65 | 66 | if (__DEVELOPMENT__ && module.hot) { 67 | 68 | // CORE REDUCERS 69 | module.hot.accept('../universal/redux/core', () => { 70 | console.log("CORE"); 71 | reducerRegistry.updateReducers(store, require('../universal/redux/core').default); 72 | }); 73 | 74 | // ADDITIONAL REDUCERS 75 | // These reducers are required from the Routes.tsx file so we must also accept 76 | // a new routes file 77 | module.hot.accept('../universal/Routes', () => {}); 78 | 79 | // Product Page 80 | System.import('../universal/redux/modules/productPage') 81 | module.hot.accept('../universal/redux/modules/productPage', () => { 82 | console.log("PRODUCT PAGE"); 83 | System.import('../universal/redux/modules/productPage') 84 | .then((reducer) => { 85 | reducerRegistry.updateReducers(store, { 'productPage': reducer.default }) 86 | }); 87 | }); 88 | } 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /typescript/config.ts: -------------------------------------------------------------------------------- 1 | declare const process: any; 2 | 3 | const environment = { 4 | development: { 5 | isProduction: false 6 | }, 7 | production: { 8 | isProduction: true 9 | } 10 | }[process.env.NODE_ENV || 'development']; 11 | 12 | export default Object.assign({ 13 | // host: process.env.HOST || 'localhost', 14 | // port: process.env.PORT, 15 | // apiHost: process.env.APIHOST || 'localhost', 16 | // apiPort: process.env.APIPORT, 17 | app: { 18 | title: 'Redux shopping cart example', 19 | description: 'Isomorphic React with lazy loading.', 20 | head: { 21 | titleTemplate: 'React Redux Example: %s', 22 | meta: [ 23 | {name: 'description', content: 'All the modern best practices in one example.'}, 24 | {charset: 'utf-8'}, 25 | {property: 'og:site_name', content: 'React Redux Example'}, 26 | {property: 'og:image', content: 'https://react-redux.herokuapp.com/logo.jpg'}, 27 | {property: 'og:locale', content: 'en_US'}, 28 | {property: 'og:title', content: 'React Redux Example'}, 29 | {property: 'og:description', content: 'All the modern best practices in one example.'}, 30 | {property: 'og:card', content: 'summary'}, 31 | {property: 'og:site', content: '@erikras'}, 32 | {property: 'og:creator', content: '@erikras'}, 33 | {property: 'og:title', content: 'React Redux Example'}, 34 | {property: 'og:description', content: 'All the modern best practices in one example.'}, 35 | {property: 'og:image', content: 'https://react-redux.herokuapp.com/logo.jpg'}, 36 | {property: 'og:image:width', content: '200'}, 37 | {property: 'og:image:height', content: '200'} 38 | ] 39 | } 40 | }, 41 | 42 | }, environment); 43 | -------------------------------------------------------------------------------- /typescript/server/Html.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | const Helmet = require('react-helmet'); 4 | 5 | 6 | /** 7 | * Wrapper component containing HTML metadata and boilerplate tags. 8 | * Used in server-side code only to wrap the string output of the 9 | * rendered route component. 10 | * 11 | * The only thing this component doesn't (and can't) include is the 12 | * HTML doctype declaration, which is added to the rendered output 13 | * by the server.js file. 14 | */ 15 | 16 | interface HTMLProps { 17 | assets: any; 18 | component: any; 19 | store: any; 20 | } 21 | 22 | export default class Html extends React.Component { 23 | 24 | render() : React.ReactElement { 25 | const {assets, component, store} = this.props; 26 | let content = component ? renderToString(component) : ''; 27 | let head = Helmet.rewind(); 28 | 29 | return ( 30 | 31 | 32 | Redux shopping cart examples 33 | {head.base.toComponent()} 34 | {head.title.toComponent()} 35 | {head.meta.toComponent()} 36 | {head.link.toComponent()} 37 | {head.script.toComponent()} 38 | 39 | 40 | 41 | {/* styles (will be present only in production with webpack extract text plugin) */} 42 | {Object.keys(assets.styles).map((style, key) => 43 | 45 | )} 46 | 47 | {/* (will be present only in development mode) */} 48 | {/* outputs a