diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..2b2a2a2 --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + "env", + "react" + ], + "plugins": [ + "transform-class-properties", + "transform-object-rest-spread" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad46b30..9a3a4fd 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,14 @@ typings/ .yarn-integrity # dotenv environment variables file -.env +.env.* # next.js build output .next + +# VS Code files +.vscode/ + +# generated distribution files +public/dist/ +dist/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..c1cdf6d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/carbon \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a24ba86 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +# Start with nginx alpine +FROM nginx:1.14-alpine +# Copy nginx configuration file(s) +COPY etc/nginx/*.conf /etc/nginx/conf.d/ +# Remove default nginx configuration file +RUN rm /etc/nginx/conf.d/default.conf +# Copy dist/ to /usr/share/nginx/html/ +COPY dist/ /usr/share/nginx/html/ +# Application should be accessible on port 80 +# Note: This is the default port exposed by the nginx image diff --git a/README.md b/README.md index a7b4fd4..ff7fe2f 100644 --- a/README.md +++ b/README.md @@ -1 +1,221 @@ -# skeleton-ui-react \ No newline at end of file +# React Starter Project + +## Acknowledgements + +This is a [LEAN**STACKS**](https://leanstacks.com/) solution. + +## Getting Started + +This is a Single-Page Application (SPA) user interface application authored in JavaScript using the [React](https://reactjs.org/) framework. + +## Languages + +This project is primarily authored in: + +* ECMAScript 2017 (JavaScript 8th Edition) with syntatic sugar via Babel +* HTML +* SASS + +**Note:** Babel allows developers the flexibility to choose the 6th, 7th, or 8th edition of JavaScript. The Babel transpiler ensures a browser-compatible build. + +## Installation + +### Fork the Repository + +Fork the [GitHub repo](https://github.com/leanstacks/skeleton-ui-react). Clone the project to the host machine. + +### Dependencies + +This project requires the following global dependencies on the host machine: + +* Node 6+ +* NPM 3+ +* Yarn 1.3+ + +**Note:** This project has been tested with Node 8.11+ (Carbon) and 6.14+ (Boron). + +After installing the global dependencies, initialize the project. Open a terminal window, navigate to the project base directory and issue this command: + +``` +yarn install +``` + +Yarn retrieves all project dependencies and installs them into the `/node_modules` sub-directory. + +### Editors + +You may use your preferred text editor. [Atom](https://atom.io/) or [VS Code](https://code.visualstudio.com/) are recommended. + +## Running + +The project uses [Yarn commands](https://yarnpkg.com/lang/en/docs/cli/) for build, test, and local debugging workflow automation. The following Yarn commands are defined. + +### Start + +The **start** command performs the following workflow steps: + +* starts the Webpack development server +* builds the application and loads it into memory +* watches source directories for changes +* republishes source files when changes occur +* reloads the application in the browser when changed source files are republished + +The **start** command is designed to allow engineers the means to rapidly make application changes on their local machines. This task is not intended for use in a server environment. + +To execute the **start** command, type the following at a terminal prompt in the project base directory: + +``` +yarn start +``` + +Open a browser and go to http://localhost:9000/ to use the application. + +To stop the Webpack development server, press `ctrl-C` in the terminal window. + +### Test + +The **test** command performs the following workflow steps: + +* executes tests once and exits + +The **test** command is designed to allow engineers the means to run all tests contained within `*.test.js` files located in the `/src/tests/` sub-directory. + +To execute the **test** command, type the following at a terminal prompt in the project base directory: + +``` +yarn test +``` + +To start the test environment and re-execute tests as source files are modified, use the `--watch` option. + +``` +yarn test --watch +``` + +To stop the test environment in watch mode, press `q` in the terminal window. + +### Build + +The **build** command performs the following workflow steps: + +* starts the Webpack process +* creates a clean distribution `/dist` directory +* copies all static assets to the distribution directory +* transpiles, ugilifies, minifies, and maps source files into distribution bundles +* injects the distribution bundles into `link` and `script` tags within the `index.html` file + +To execute the **build** command, type the following at a terminal prompt in the project base directory: + +``` +yarn build +``` + +The **build** command has environment-specific variants which allow for the injection of alternative values into environment variables via the [Webpack Define Plugin](https://webpack.js.org/plugins/define-plugin/). See the Define Plugin documentation for more information. + +To execute the **build** command for a configured environment, type the following command at a terminal prompt in the base directory: + +``` +yarn build:dev + +OR + +yarn build:qa +``` + +## Deployment + +This project is ideally suited to be hosted from a static web server (e.g. Apache or Nginx) or from a CDN (e.g. AWS CloudFront). + +To prepare the application distribution for deployment, run the **build** Yarn command documented above. Next, take all of the files and directories from the `/dist` directory and deploy them to your hosting environment. + +### Web Server Configuration + +#### Fallback to index.html + +Routed applications must fall back to `index.html`. That means, if you are using SPA routing you must configure the static web server to return to the base html page (`index.html`) when the router is asked to serve a route which does not exist. + +A static web server commonly returns `index.html` when it receives a request for `http://www.example.com/`. But it returns a `404 - Not Found` error when processing `http://www.example.com/greetings/109` unless it is configured to return `index.html` instead. + +Each static web server is configured for fallback in a different way. Here are a few examples for common scenarios. + +##### Webpack Development Server + +``` +historyApiFallback: { + disableDotRule: true, + htmlAcceptHeaders: [text/html', 'application/xhtml+xml'] +} +``` + +##### Apache + +Add a rewrite rule to the `.htaccess` file as illustrated below. + +``` +RewriteEngine On + # If an existing asset or directory is requested, go to it as it is + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR] + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d + RewriteRule ^ - [L] + # If the requested resource doesn't exist, use index.html + RewriteRule ^ /index.html +``` + +##### NGinx + +Use `try_files` as described in the [Front Controller Pattern Web Apps](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#front-controller-pattern-web-apps) documentation. + +``` +try_files $uri $uri/ /index.html; +``` + +##### IIS + +Add a rewrite rule to `web.config`, similar to the one illustrated below. + +``` + + + + + + + + + + + + + + +``` + +## Technology Stacks + +### Application + +[React](https://reactjs.org/) +[Redux](https://redux.js.org/) +[React Router](https://reacttraining.com/react-router/) +[Axios](https://github.com/axios/axios) +[Lodash](https://lodash.com/) +[Moment](https://momentjs.com/) +[Numeral](http://numeraljs.com/) +[Bootstrap](https://getbootstrap.com/) +[Font Awesome](https://fontawesome.com/) +[Google Fonts](https://fonts.google.com) +[JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +[SASS](http://sass-lang.com/guide) + +### Test + +[Jest](http://jestjs.io/) +[Enzyme](http://airbnb.io/enzyme/) +[Redux Mock Store](https://www.npmjs.com/package/redux-mock-store) + +### Build + +[Babel](http://babeljs.io/) +[Node.js](https://nodejs.org/) +[Webpack](https://webpack.js.org/configuration/) +[Yarn](https://yarnpkg.com/en/) diff --git a/etc/nginx/spa.conf b/etc/nginx/spa.conf new file mode 100644 index 0000000..2dcf460 --- /dev/null +++ b/etc/nginx/spa.conf @@ -0,0 +1,13 @@ +# Basic NGINX configuration for a Single-Page Application (SPA) +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + + # This try_files statement facilitates SPA framework routing + try_files $uri $uri/ /index.html; + } +} diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 0000000..d151d63 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,10 @@ +{ + "setupFiles": [ + "raf/polyfill", + "jest-localstorage-mock", + "/src/tests/setupTests.js" + ], + "snapshotSerializers": [ + "enzyme-to-json/serializer" + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..23fb7fd --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "skeleton-ui-react", + "version": "0.5.0", + "description": "React SPA Starter Project", + "main": "index.js", + "repository": "git@github.com:leanstacks/skeleton-ui-react.git", + "author": "Matt Warman ", + "license": "MIT", + "private": false, + "scripts": { + "build:dev": "webpack --config webpack.dev.js", + "build:qa": "webpack --config webpack.qa.js", + "build": "webpack --config webpack.prod.js", + "start": "webpack-dev-server --config webpack.dev.js", + "test": "jest --config=jest.config.json" + }, + "dependencies": {}, + "devDependencies": { + "@fortawesome/fontawesome-free": "^5.1.0", + "axios": "^0.17.1", + "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "bootstrap": "4.1.1", + "clean-webpack-plugin": "^0.1.17", + "copy-webpack-plugin": "^4.3.1", + "css-loader": "^0.28.7", + "dotenv": "^4.0.0", + "enzyme": "^3.2.0", + "enzyme-adapter-react-16": "^1.1.0", + "enzyme-to-json": "^3.2.2", + "extract-text-webpack-plugin": "^3.0.2", + "file-loader": "^1.1.6", + "history": "^4.7.2", + "html-webpack-plugin": "^2.30.1", + "jest": "^21.2.1", + "jest-localstorage-mock": "^2.2.0", + "jquery": "^3.2.1", + "lodash": "^4.17.10", + "moment": "^2.19.3", + "node-sass": "^4.7.2", + "numeral": "^2.0.6", + "popper.js": "^1.14.3", + "raf": "^3.4.0", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "react-redux": "^5.0.6", + "react-router-dom": "^4.2.2", + "redux": "^3.7.2", + "redux-mock-store": "^1.3.0", + "redux-thunk": "^2.2.0", + "sass-loader": "^6.0.6", + "style-loader": "^0.19.0", + "url-loader": "^0.6.2", + "validator": "^9.1.2", + "webpack": "^3.9.1", + "webpack-dev-server": "^2.9.5", + "webpack-merge": "^4.1.1" + } +} diff --git a/public/assets/data/technologies/technologies.json b/public/assets/data/technologies/technologies.json new file mode 100644 index 0000000..5aed35f --- /dev/null +++ b/public/assets/data/technologies/technologies.json @@ -0,0 +1,168 @@ +[ + { + "id": "adbe711c-9698-41f7-bf86-ae3e41ba49eb", + "name": "React", + "type": "application", + "version": "16.2.0", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "React is a structural framework for dynamic web apps.", + "url": "https://reactjs.org/" + }, + { + "id": "0360ff87-0f87-411e-8231-797806583067", + "name": "Redux", + "type": "application", + "version": "3.7.2", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Redux is a predictable state container for JavaScript apps.", + "url": "https://redux.js.org/" + }, + { + "id": "805356dc-f453-4842-89c8-7d763310c5a3", + "name": "React Router", + "type": "application", + "version": "4.2.2", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Components are the heart of React's powerful, declarative programming model. React Router is a collection of navigational components that compose declaratively with your application.", + "url": "https://reacttraining.com/react-router/" + }, + { + "id": "4b5b3180-2169-4d80-a22c-1ca4248cb049", + "name": "Axios", + "type": "application", + "version": "0.17.1", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "A Promise-based HTTP client for the browser and node.js.", + "url": "https://github.com/axios/axios" + }, + { + "id": "3b5968c7-6109-428b-bc2b-62652d8c6d8d", + "name": "Bootstrap", + "type": "application", + "version": "4.1.1", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Bootstrap is an open source toolkit for developing with HTML, CSS, and JS.", + "url": "https://getbootstrap.com/" + }, + { + "id": "7d09624e-0256-4fe5-9b9f-bf76683536a1", + "name": "Font Awesome", + "type": "application", + "version": "5.1.0", + "license": "Multiple", + "licenseUrl": "https://fontawesome.com/license", + "description": "Font Awesome is an icon toolkit.", + "url": "https://fontawesome.com/" + }, + { + "id": "8f2d877d-d17a-4b3c-bbc3-690f460a57e0", + "name": "Lodash", + "type": "application", + "version": "4.17.10", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "A modern JavaScript utility library delivering modularity, performance & extras.", + "url": "https://lodash.com/" + }, + { + "id": "aa636860-0fa5-4093-9592-1cbb6b2f5c3b", + "name": "Moment", + "type": "application", + "version": "2.19.3", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Parse, validate, manipulate, and display dates and times in JavaScript.", + "url": "https://momentjs.com/" + }, + { + "id": "e7b38c23-e22d-4cc3-9626-dffce5c5bd2a", + "name": "Numeral", + "type": "application", + "version": "2.0.6", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "A javascript library for formatting and manipulating numbers.", + "url": "http://numeraljs.com/" + }, + { + "id": "d142466a-e461-474b-9aac-3490a37906d8", + "name": "Webpack", + "type": "build", + "version": "3.9.1", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Webpack is a static module bundler for modern web applications.", + "url": "https://webpack.js.org/" + }, + { + "id": "7671e7af-7dc6-4d22-ba65-6a21778d2beb", + "name": "Babel", + "type": "build", + "version": "6.26.0", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Babel is a JavaScript transpiler.", + "url": "https://babeljs.io/" + }, + { + "id": "126d771c-b606-4c10-866b-4385e44b15d3", + "name": "Node.js", + "type": "build", + "version": "8.11.3", + "description": "Node.js is a JavaScript runtime.", + "url": "https://nodejs.org/" + }, + { + "id": "079dd1b7-f5b1-4c32-a0af-c7ea65e94976", + "name": "NPM", + "type": "build", + "version": "5.6.0", + "description": "npm is the Node.js package manager.", + "url": "https://www.npmjs.com/" + }, + { + "id": "6cab5852-7750-4af3-b5dc-59220f2ca03e", + "name": "Yarn", + "type": "build", + "version": "1.7.0", + "license": "BSD 2-Clause", + "licenseUrl": "https://spdx.org/licenses/BSD-2-Clause.html", + "description": "Yarn is an alternative to the npm client.", + "url": "https://yarnpkg.com/" + }, + { + "id": "8218cb49-7830-4d88-8df5-4c1ad4d4df4f", + "name": "Jest", + "type": "test", + "version": "21.2.1", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Jest is used by Facebook to test all JavaScript code including React applications.", + "url": "http://jestjs.io/" + }, + { + "id": "f62dd51f-ceae-4051-bbf1-931161ee54fc", + "name": "Enzyme", + "type": "test", + "version": "3.2.0", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.", + "url": "http://airbnb.io/enzyme/" + }, + { + "id": "878a061d-2605-4839-ba44-8b2eced0e982", + "name": "Redux Mock Store", + "type": "test", + "version": "1.3.0", + "license": "MIT", + "licenseUrl": "https://spdx.org/licenses/MIT.html", + "description": "A mock store for testing Redux async action creators and middleware.", + "url": "https://www.npmjs.com/package/redux-mock-store" + } +] \ No newline at end of file diff --git a/public/assets/img/favicon.png b/public/assets/img/favicon.png new file mode 100644 index 0000000..0a919c4 Binary files /dev/null and b/public/assets/img/favicon.png differ diff --git a/public/assets/img/react-icon.svg b/public/assets/img/react-icon.svg new file mode 100644 index 0000000..5592ebe --- /dev/null +++ b/public/assets/img/react-icon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/src/actions/technologies.js b/src/actions/technologies.js new file mode 100644 index 0000000..6f57477 --- /dev/null +++ b/src/actions/technologies.js @@ -0,0 +1,33 @@ +import axios from 'axios'; + +import config from '../config/environment'; + +// SET_TECHNOLOGIES +export const setTechnologies = (technologies) => ({ + type: 'SET_TECHNOLOGIES', + technologies +}); + +const apiUrl = config.apiUrl || ''; + +// START_SET_TECHNOLOGIES +export const startSetTechnologies = () => { + return (dispatch) => { + const config = { + url: `${apiUrl}/assets/data/technologies/technologies.json`, + method: 'get', + headers: { + 'Accept': 'application/json' + } + }; + return axios.request(config) + .then((response) => { + console.log(`response: ${JSON.stringify(response, null, 2)}`); + dispatch(setTechnologies(response.data)); + localStorage.setItem('technologies', JSON.stringify(response.data)); + localStorage.setItem('technologies_lu', Date.now()); + }).catch((err) => { + console.error('API error. ', err); + }); + }; +}; diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..74a9539 --- /dev/null +++ b/src/app.js @@ -0,0 +1,58 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import moment from 'moment'; + +import AppRouter, { history } from './routers/AppRouter'; +import configureStore from './store/configureStore'; + +import LoadingPage from './components/LoadingPage'; + +import { startSetTechnologies, setTechnologies } from './actions/technologies'; + +import 'bootstrap/dist/js/bootstrap'; +import 'bootstrap/dist/css/bootstrap.css'; +import '@fortawesome/fontawesome-free/js/all'; +import './styles/styles.scss'; + +const store = configureStore(); + +const jsx = ( + + + +); +let hasRendered = false; +const renderApp = () => { + if (!hasRendered) { + ReactDOM.render(jsx, document.getElementById('app')); + hasRendered = true; + } +}; + +ReactDOM.render(, document.getElementById('app')); + +// Initialize the Application State +const technologiesLastUpdated = (localStorage.getItem('technologies_lu')) ? moment(Number.parseInt(localStorage.getItem('technologies_lu'))) : moment(0); +console.log('technologiesLastUpdated', technologiesLastUpdated.format()); +const technologiesState = (localStorage.getItem('technologies')) ? JSON.parse(localStorage.getItem('technologies')) : null; +if (technologiesLastUpdated.add(1, 'hours').isBefore(moment())) { + // Cached Data is Stale or Does Not Exist + console.log('Cached Data is Stale or Does Not Exist'); + // Fetch Data using API + store.dispatch(startSetTechnologies()).then(() => { + console.log('startSetTechnologies success'); + renderApp(); + }).catch((err) => { + //TODO Handle API Failure + console.log('startSetTechnologies failure'); + }); +} else if (technologiesState) { + // Cached Data Exists and is Current + console.log('Cached Data Exists and is Current'); + store.dispatch(setTechnologies(technologiesState)); + renderApp(); +} else { + renderApp(); + history.push('/landing'); +} diff --git a/src/components/BusyIndicator.js b/src/components/BusyIndicator.js new file mode 100644 index 0000000..67496c4 --- /dev/null +++ b/src/components/BusyIndicator.js @@ -0,0 +1,32 @@ +import React from 'react'; + +const getSizeClass = (size) => { + switch (size) { + case 'xl': + return 'busy-indicator--xl'; + case 'lg': + return 'busy-indicator--lg'; + case 'sm': + return 'busy-indicator--sm'; + default: + return 'busy-indicator--md'; + }; +}; + +const BusyIndicator = (props) => { + const icon = props.icon || 'fa-circle-o-notch'; + return ( +
+
+
+
{props.title}
+
+
+ +
+
+
+ ); +}; + +export default BusyIndicator; diff --git a/src/components/CurrentMoment.js b/src/components/CurrentMoment.js new file mode 100644 index 0000000..eff5d29 --- /dev/null +++ b/src/components/CurrentMoment.js @@ -0,0 +1,34 @@ +import React from 'react'; +import moment from 'moment'; + +export default class CurrentMoment extends React.Component { + + constructor(props) { + super(props); + + this.state = { + format: props.format || 'M/D/YY h:mm A', + now: moment() + }; + } + + componentDidMount() { + this.intervalId = setInterval(this.refreshCurrentMoment, 5000); + } + + componentWillUnmount() { + clearInterval(this.intervalId); + } + + refreshCurrentMoment = () => { + const now = moment(); + this.setState(() => ({ now })); + }; + + render() { + return ( + {this.state.now.format(this.state.format)} + ); + } + +}; \ No newline at end of file diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 0000000..6407dfc --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,32 @@ +import React from 'react'; + +import CurrentMoment from './CurrentMoment'; + +const Footer = () => ( +
+
+
+
+
    +
  • React Starter Project
  • +
  • A LEANSTACKS Solution
  • +
  • © 
  • +
+
+
+
    +
  • +
+
+
+
    +
  • Terms of Use
  • +
  • Privacy Policy
  • +
+
+
+
+
+); + +export default Footer; diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..25b01c0 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +const Header = () => ( +
+ +
+); + +export default Header; diff --git a/src/components/LandingPage.js b/src/components/LandingPage.js new file mode 100644 index 0000000..05acf54 --- /dev/null +++ b/src/components/LandingPage.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const LandingPage = () => ( +
+
+
+
+

Hello React LogoReact

+

Hello
React LogoReact

+

Hello
React LogoReact

+

Hello
React LogoReact

+
+
+
+
+

+ Welcome to the LEANSTACKS React starter project. + This project provides a template to kickstart React single-page applications + utilizing a curated Technology Stack for optimal testability, maintainability, + and operability. +

+
+
+
+
+); + +export default LandingPage; diff --git a/src/components/LoadingPage.js b/src/components/LoadingPage.js new file mode 100644 index 0000000..4e9ad61 --- /dev/null +++ b/src/components/LoadingPage.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import BusyIndicator from './BusyIndicator'; + +const LoadingPage = () => ( +
+ +
+); + +export default LoadingPage; diff --git a/src/components/NotFoundPage.js b/src/components/NotFoundPage.js new file mode 100644 index 0000000..d907bad --- /dev/null +++ b/src/components/NotFoundPage.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const NotFoundPage = () => ( +
+
+
+
+

+  Not Found +

+

Return to the stacks.

+
+
+
+
+); + +export default NotFoundPage; diff --git a/src/components/Stack.js b/src/components/Stack.js new file mode 100644 index 0000000..2cead02 --- /dev/null +++ b/src/components/Stack.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { getTechnologiesByType } from '../selectors/technologies'; + +export const Stack = (props) => { + return ( +
+

{props.name} Stack

+
    + { + props.technologies.map((technology) => ( +
  • {technology.name}
  • + )) + } +
+ Learn More  +
+ ); +} + +const mapStateToProps = (state, props) => ({ + technologies: getTechnologiesByType(state.technologies, props.type) +}); + +export default connect(mapStateToProps)(Stack); \ No newline at end of file diff --git a/src/components/StackDetailPage.js b/src/components/StackDetailPage.js new file mode 100644 index 0000000..0c5da7c --- /dev/null +++ b/src/components/StackDetailPage.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import _ from 'lodash'; + +import { getTechnologiesByType, getTechnology } from '../selectors/technologies'; +import Technology from './Technology'; + +export const StackDetailPage = (props) => ( +
+
+

{_.capitalize(props.stackType)} Stack

+ +
+
+
Select any technology to view additional detail.
+
+
+ +
+
+

Technologies

+
    + { + props.technologies.map((technology) => ( +
  • + {technology.name} +
  • + )) + } +
+
+  Back +
+
+ + { props.technology && +
+
+ +
+
+ +
+
+ } +
+
+
+); + +const mapStateToProps = (state, props) => ({ + stackType: props.match.params.stackType, + technologies: getTechnologiesByType(state.technologies, props.match.params.stackType), + technology: getTechnology(state.technologies, props.match.params.technologyId) +}); + +export default connect(mapStateToProps)(StackDetailPage); \ No newline at end of file diff --git a/src/components/StacksPage.js b/src/components/StacksPage.js new file mode 100644 index 0000000..293db87 --- /dev/null +++ b/src/components/StacksPage.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import Stack from './Stack'; + +const StacksPage = (props) => ( +
+
+

Technology Stacks

+
+
+ +
+
+ +
+
+ +
+
+
+
+); + + +export default StacksPage; \ No newline at end of file diff --git a/src/components/Technology.js b/src/components/Technology.js new file mode 100644 index 0000000..66ae453 --- /dev/null +++ b/src/components/Technology.js @@ -0,0 +1,28 @@ +import React from 'react'; + +const Technology = (props) => ( +
+
+

{props.technology.name}

+ { props.technology.version && +
Version: {props.technology.version}
+ } + { props.technology.license && +
+ License: {props.technology.license} + { props.technology.licenseUrl && +   + } +
+ } +
{props.technology.description}
+ { props.technology.url && + + } +
+
+); + +export default Technology; \ No newline at end of file diff --git a/src/config/environment.js b/src/config/environment.js new file mode 100644 index 0000000..203a25b --- /dev/null +++ b/src/config/environment.js @@ -0,0 +1,3 @@ +export default { + apiUrl: process.env.API_URL +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..6090410 --- /dev/null +++ b/src/index.html @@ -0,0 +1,16 @@ + + + + + + + React Starter Project | LeanStacks + + + + + +
+ + + \ No newline at end of file diff --git a/src/reducers/technologies.js b/src/reducers/technologies.js new file mode 100644 index 0000000..22b2898 --- /dev/null +++ b/src/reducers/technologies.js @@ -0,0 +1,14 @@ +const defaultState = { + technologies: [] +}; + +export default (state = defaultState, action) => { + switch (action.type) { + case 'SET_TECHNOLOGIES': + return { + technologies: action.technologies + }; + default: + return state; + }; +}; diff --git a/src/routers/AppRouter.js b/src/routers/AppRouter.js new file mode 100644 index 0000000..31490b2 --- /dev/null +++ b/src/routers/AppRouter.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; +import createHistory from 'history/createBrowserHistory'; + +import Footer from '../components/Footer'; +import Header from '../components/Header'; +import LandingPage from '../components/LandingPage'; +import NotFoundPage from '../components/NotFoundPage'; +import StackDetailPage from '../components/StackDetailPage'; +import StacksPage from '../components/StacksPage'; + +export const history = createHistory(); + +const AppRouter = () => ( + +
+
+ + + + + + + + +
+
+
+); + +export default AppRouter; diff --git a/src/selectors/technologies.js b/src/selectors/technologies.js new file mode 100644 index 0000000..2a17cd9 --- /dev/null +++ b/src/selectors/technologies.js @@ -0,0 +1,7 @@ +export const getTechnologiesByType = (technologies = [], type) => { + return technologies.filter((technology) => technology.type === type); +}; + +export const getTechnology = (technologies = [], technologyId) => { + return technologies.find((technology) => technology.id === technologyId); +}; diff --git a/src/store/configureStore.js b/src/store/configureStore.js new file mode 100644 index 0000000..afe5eb4 --- /dev/null +++ b/src/store/configureStore.js @@ -0,0 +1,15 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; + +import stacksReducer from '../reducers/technologies'; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +export default () => { + const store = createStore( + stacksReducer, + composeEnhancers(applyMiddleware(thunk)) + ); + + return store; +}; diff --git a/src/styles/base/_base.scss b/src/styles/base/_base.scss new file mode 100644 index 0000000..a842a9b --- /dev/null +++ b/src/styles/base/_base.scss @@ -0,0 +1,33 @@ +body { + background: $off-white; + color: $black; + font-family: $font-primary; +} + +h1, h2, .h1, .h2 { + font-family: $font-secondary; +} + +a { + color: $blue; + + &:hover { + color: $blue; + } +} + +.page { + min-height: 75vh; +} + +.text-blue { + color: $blue; +} + +.text-dark-grey { + color: $dark-grey; +} + +.text-react-blue { + color: $react-blue; +} \ No newline at end of file diff --git a/src/styles/base/_settings.scss b/src/styles/base/_settings.scss new file mode 100644 index 0000000..787b6a2 --- /dev/null +++ b/src/styles/base/_settings.scss @@ -0,0 +1,28 @@ +// coolors +$off-white: #f7f7f7; +$black: #090c08; +$blue: #2196f3; +$dark-grey: #343a40; +$react-blue: #36d9f8; + +// Font Families +$font-primary: 'Open Sans', sans-serif; +$font-secondary: 'Raleway', sans-serif; + +// Font Sizes +$font-size-xsmall: 0.8rem; +$font-size-small: 0.9rem; +$font-size-medium: 1.0rem; +$font-size-large: 1.6rem; +$font-size-xlarge: 2.4rem; +$font-size-xxlarge: 3.6rem; + +// Spacing +$s-size: 1.2rem; +$m-size: 1.6rem; +$l-size: 3.2rem; +$xl-size: 4.8rem; +$desktop-breakpoint: 45rem; +$screen-sm-min: 768px; +$screen-md-min: 992px; +$screen-lg-min: 1200px; diff --git a/src/styles/components/_busy-indicator.scss b/src/styles/components/_busy-indicator.scss new file mode 100644 index 0000000..031658e --- /dev/null +++ b/src/styles/components/_busy-indicator.scss @@ -0,0 +1,43 @@ +.busy-indicator { + align-items: center; + display: flex; + justify-content: center; + height: 100%; + width: 100%; +} + +.busy-indicator__content { + align-items: center; + display: flex; + justify-content: center; +} + +.busy-indicator__icon { + margin-left: $s-size; +} + +.busy-indicator--xl { + font-size: $font-size-xxlarge; +} + +.busy-indicator--lg { + font-size: $font-size-xlarge; +} + +.busy-indicator--md { + +} + +.busy-indicator--sm { + font-size: $font-size-xsmall; +} + +.busy-indicator--full-page { + height: 100vh; + width: 100vw; +} + +.busy-indicator--half-page { + height: 50vh; + width: 100vw; +} diff --git a/src/styles/components/_footer.scss b/src/styles/components/_footer.scss new file mode 100644 index 0000000..933af9a --- /dev/null +++ b/src/styles/components/_footer.scss @@ -0,0 +1,4 @@ +footer { + border-top: 2px solid darken($dark-grey, 7%); + font-size: $font-size-xsmall; +} diff --git a/src/styles/styles.scss b/src/styles/styles.scss new file mode 100644 index 0000000..70926d6 --- /dev/null +++ b/src/styles/styles.scss @@ -0,0 +1,4 @@ +@import './base/settings'; +@import './base/base'; +@import './components/busy-indicator'; +@import './components/footer'; diff --git a/src/tests/__mocks__/axios.js b/src/tests/__mocks__/axios.js new file mode 100644 index 0000000..dd8a0bb --- /dev/null +++ b/src/tests/__mocks__/axios.js @@ -0,0 +1,3 @@ +export default { + request: jest.fn(() => Promise.resolve({ data: {} })) +}; \ No newline at end of file diff --git a/src/tests/__mocks__/moment.js b/src/tests/__mocks__/moment.js new file mode 100644 index 0000000..0f17c80 --- /dev/null +++ b/src/tests/__mocks__/moment.js @@ -0,0 +1,8 @@ +const moment = require.requireActual('moment'); + +const now = () => { + return 0; +}; +moment.now = now; + +export default moment; diff --git a/src/tests/actions/technologies.test.js b/src/tests/actions/technologies.test.js new file mode 100644 index 0000000..f25ba1e --- /dev/null +++ b/src/tests/actions/technologies.test.js @@ -0,0 +1,44 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import mockAxios from 'axios'; + +import { + setTechnologies, + startSetTechnologies +} from '../../actions/technologies'; + +import technologies from '../fixtures/technologies'; + +const defaultState = { + technologies: [] +}; +const createMockStore = configureMockStore([thunk]); + +test('should setup setTechnologies action object', () => { + const action = setTechnologies(technologies); + + expect(action).toEqual({ + type: 'SET_TECHNOLOGIES', + technologies + }); +}); + +test('should fetch technologies from API', (done) => { + const store = createMockStore(defaultState); + mockAxios.request.mockImplementationOnce(() => + Promise.resolve({ + data: technologies + }) + ); + + store.dispatch(startSetTechnologies()).then(() => { + const actions = store.getActions(); + expect(actions[0]).toEqual({ + type: 'SET_TECHNOLOGIES', + technologies + }); + + done(); + }); +}); \ No newline at end of file diff --git a/src/tests/components/BusyIndicator.test.js b/src/tests/components/BusyIndicator.test.js new file mode 100644 index 0000000..d11a06c --- /dev/null +++ b/src/tests/components/BusyIndicator.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import BusyIndicator from '../../components/BusyIndicator'; + +test('should render BusyIndicator correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/src/tests/components/CurrentMoment.test.js b/src/tests/components/CurrentMoment.test.js new file mode 100644 index 0000000..1dbaf22 --- /dev/null +++ b/src/tests/components/CurrentMoment.test.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import CurrentMoment from '../../components/CurrentMoment'; + +test('should render CurrentMoment correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +test('should render CurrentMoment with format correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/src/tests/components/Footer.test.js b/src/tests/components/Footer.test.js new file mode 100644 index 0000000..22a8996 --- /dev/null +++ b/src/tests/components/Footer.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Footer from '../../components/Footer'; + +test('should render Footer correctly', () => { + const wrapper = shallow(