diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..2a95a0e
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1,10 @@
+# Node.JS Specific
+dist/
+node_modules/
+package-lock.json
+
+# VSCode
+.vscode/
+
+# MacOS
+.DS_Store
diff --git a/client/LICENSE b/client/LICENSE
new file mode 100644
index 0000000..edacd8e
--- /dev/null
+++ b/client/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Valentino Gagliardi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..027d140
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Cars
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..b56ec4c
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "reactreduxapp",
+ "version": "1.0.0",
+ "description": "React Redux App",
+ "repository": "git://github.com",
+ "main": "./src/index.js",
+ "scripts": {
+ "start": "webpack --watch --mode=development & webpack-dev-server --hot --mode=development",
+ "build": "webpack --mode=production"
+ },
+ "author": "YJ",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "^16.3.17",
+ "@types/react-dom": "^16.0.6",
+ "@types/react-redux": "^6.0.2",
+ "react": "^16.4.1",
+ "react-dom": "^16.4.1",
+ "react-redux": "^5.0.7",
+ "redux": "^4.0.0",
+ "request": "^2.87.0"
+ },
+ "devDependencies": {
+ "@types/request": "^2.47.1",
+ "@types/uuid": "^3.4.3",
+ "awesome-typescript-loader": "^5.0.0",
+ "json-loader": "^0.5.7",
+ "source-map-loader": "^0.2.3",
+ "typescript": "^2.9.2",
+ "webpack": "^4.12.0",
+ "webpack-cli": "^3.0.7",
+ "webpack-dev-server": "^3.1.4"
+ }
+}
diff --git a/client/src/assets/css/fa-svg-with-js.css b/client/src/assets/css/fa-svg-with-js.css
new file mode 100644
index 0000000..264ae48
--- /dev/null
+++ b/client/src/assets/css/fa-svg-with-js.css
@@ -0,0 +1,345 @@
+/*!
+ * Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com
+ * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+ */
+svg:not(:root).svg-inline--fa {
+ overflow: visible; }
+
+.svg-inline--fa {
+ display: inline-block;
+ font-size: inherit;
+ height: 1em;
+ overflow: visible;
+ vertical-align: -.125em; }
+ .svg-inline--fa.fa-lg {
+ vertical-align: -.225em; }
+ .svg-inline--fa.fa-w-1 {
+ width: 0.0625em; }
+ .svg-inline--fa.fa-w-2 {
+ width: 0.125em; }
+ .svg-inline--fa.fa-w-3 {
+ width: 0.1875em; }
+ .svg-inline--fa.fa-w-4 {
+ width: 0.25em; }
+ .svg-inline--fa.fa-w-5 {
+ width: 0.3125em; }
+ .svg-inline--fa.fa-w-6 {
+ width: 0.375em; }
+ .svg-inline--fa.fa-w-7 {
+ width: 0.4375em; }
+ .svg-inline--fa.fa-w-8 {
+ width: 0.5em; }
+ .svg-inline--fa.fa-w-9 {
+ width: 0.5625em; }
+ .svg-inline--fa.fa-w-10 {
+ width: 0.625em; }
+ .svg-inline--fa.fa-w-11 {
+ width: 0.6875em; }
+ .svg-inline--fa.fa-w-12 {
+ width: 0.75em; }
+ .svg-inline--fa.fa-w-13 {
+ width: 0.8125em; }
+ .svg-inline--fa.fa-w-14 {
+ width: 0.875em; }
+ .svg-inline--fa.fa-w-15 {
+ width: 0.9375em; }
+ .svg-inline--fa.fa-w-16 {
+ width: 1em; }
+ .svg-inline--fa.fa-w-17 {
+ width: 1.0625em; }
+ .svg-inline--fa.fa-w-18 {
+ width: 1.125em; }
+ .svg-inline--fa.fa-w-19 {
+ width: 1.1875em; }
+ .svg-inline--fa.fa-w-20 {
+ width: 1.25em; }
+ .svg-inline--fa.fa-pull-left {
+ margin-right: .3em;
+ width: auto; }
+ .svg-inline--fa.fa-pull-right {
+ margin-left: .3em;
+ width: auto; }
+ .svg-inline--fa.fa-border {
+ height: 1.5em; }
+ .svg-inline--fa.fa-li {
+ width: 2em; }
+ .svg-inline--fa.fa-fw {
+ width: 1.25em; }
+
+.fa-layers svg.svg-inline--fa {
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ position: absolute;
+ right: 0;
+ top: 0; }
+
+.fa-layers {
+ display: inline-block;
+ height: 1em;
+ position: relative;
+ text-align: center;
+ vertical-align: -.125em;
+ width: 1em; }
+ .fa-layers svg.svg-inline--fa {
+ -webkit-transform-origin: center center;
+ transform-origin: center center; }
+
+.fa-layers-text, .fa-layers-counter {
+ display: inline-block;
+ position: absolute;
+ text-align: center; }
+
+.fa-layers-text {
+ left: 50%;
+ top: 50%;
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ -webkit-transform-origin: center center;
+ transform-origin: center center; }
+
+.fa-layers-counter {
+ background-color: #ff253a;
+ border-radius: 1em;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ color: #fff;
+ height: 1.5em;
+ line-height: 1;
+ max-width: 5em;
+ min-width: 1.5em;
+ overflow: hidden;
+ padding: .25em;
+ right: 0;
+ text-overflow: ellipsis;
+ top: 0;
+ -webkit-transform: scale(0.25);
+ transform: scale(0.25);
+ -webkit-transform-origin: top right;
+ transform-origin: top right; }
+
+.fa-layers-bottom-right {
+ bottom: 0;
+ right: 0;
+ top: auto;
+ -webkit-transform: scale(0.25);
+ transform: scale(0.25);
+ -webkit-transform-origin: bottom right;
+ transform-origin: bottom right; }
+
+.fa-layers-bottom-left {
+ bottom: 0;
+ left: 0;
+ right: auto;
+ top: auto;
+ -webkit-transform: scale(0.25);
+ transform: scale(0.25);
+ -webkit-transform-origin: bottom left;
+ transform-origin: bottom left; }
+
+.fa-layers-top-right {
+ right: 0;
+ top: 0;
+ -webkit-transform: scale(0.25);
+ transform: scale(0.25);
+ -webkit-transform-origin: top right;
+ transform-origin: top right; }
+
+.fa-layers-top-left {
+ left: 0;
+ right: auto;
+ top: 0;
+ -webkit-transform: scale(0.25);
+ transform: scale(0.25);
+ -webkit-transform-origin: top left;
+ transform-origin: top left; }
+
+.fa-lg {
+ font-size: 1.33333em;
+ line-height: 0.75em;
+ vertical-align: -.0667em; }
+
+.fa-xs {
+ font-size: .75em; }
+
+.fa-sm {
+ font-size: .875em; }
+
+.fa-1x {
+ font-size: 1em; }
+
+.fa-2x {
+ font-size: 2em; }
+
+.fa-3x {
+ font-size: 3em; }
+
+.fa-4x {
+ font-size: 4em; }
+
+.fa-5x {
+ font-size: 5em; }
+
+.fa-6x {
+ font-size: 6em; }
+
+.fa-7x {
+ font-size: 7em; }
+
+.fa-8x {
+ font-size: 8em; }
+
+.fa-9x {
+ font-size: 9em; }
+
+.fa-10x {
+ font-size: 10em; }
+
+.fa-fw {
+ text-align: center;
+ width: 1.25em; }
+
+.fa-ul {
+ list-style-type: none;
+ margin-left: 2.5em;
+ padding-left: 0; }
+ .fa-ul > li {
+ position: relative; }
+
+.fa-li {
+ left: -2em;
+ position: absolute;
+ text-align: center;
+ width: 2em;
+ line-height: inherit; }
+
+.fa-border {
+ border: solid 0.08em #eee;
+ border-radius: .1em;
+ padding: .2em .25em .15em; }
+
+.fa-pull-left {
+ float: left; }
+
+.fa-pull-right {
+ float: right; }
+
+.fa.fa-pull-left,
+.fas.fa-pull-left,
+.far.fa-pull-left,
+.fal.fa-pull-left,
+.fab.fa-pull-left {
+ margin-right: .3em; }
+
+.fa.fa-pull-right,
+.fas.fa-pull-right,
+.far.fa-pull-right,
+.fal.fa-pull-right,
+.fab.fa-pull-right {
+ margin-left: .3em; }
+
+.fa-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear; }
+
+.fa-pulse {
+ -webkit-animation: fa-spin 1s infinite steps(8);
+ animation: fa-spin 1s infinite steps(8); }
+
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg); }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg); } }
+
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg); }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg); } }
+
+.fa-rotate-90 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg); }
+
+.fa-rotate-180 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+ -webkit-transform: rotate(180deg);
+ transform: rotate(180deg); }
+
+.fa-rotate-270 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+ -webkit-transform: rotate(270deg);
+ transform: rotate(270deg); }
+
+.fa-flip-horizontal {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+ -webkit-transform: scale(-1, 1);
+ transform: scale(-1, 1); }
+
+.fa-flip-vertical {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+ -webkit-transform: scale(1, -1);
+ transform: scale(1, -1); }
+
+.fa-flip-horizontal.fa-flip-vertical {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+ -webkit-transform: scale(-1, -1);
+ transform: scale(-1, -1); }
+
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical {
+ -webkit-filter: none;
+ filter: none; }
+
+.fa-stack {
+ display: inline-block;
+ height: 2em;
+ position: relative;
+ width: 2em; }
+
+.fa-stack-1x,
+.fa-stack-2x {
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ position: absolute;
+ right: 0;
+ top: 0; }
+
+.svg-inline--fa.fa-stack-1x {
+ height: 1em;
+ width: 1em; }
+
+.svg-inline--fa.fa-stack-2x {
+ height: 2em;
+ width: 2em; }
+
+.fa-inverse {
+ color: #fff; }
+
+.sr-only {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px; }
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+ clip: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ position: static;
+ width: auto; }
diff --git a/client/src/assets/js/fontawesome-all.min.js b/client/src/assets/js/fontawesome-all.min.js
new file mode 100644
index 0000000..a382743
--- /dev/null
+++ b/client/src/assets/js/fontawesome-all.min.js
@@ -0,0 +1,5 @@
+/*!
+ * Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com
+ * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+ */
+!function(){"use strict";var c={};try{"undefined"!=typeof window&&(c=window)}catch(c){}var l=(c.navigator||{}).userAgent,h=void 0===l?"":l,v=c,z=(~h.indexOf("MSIE")||h.indexOf("Trident/"),"___FONT_AWESOME___"),e=function(){try{return!0}catch(c){return!1}}(),m=[1,2,3,4,5,6,7,8,9,10],a=m.concat([11,12,13,14,15,16,17,18,19,20]);["xs","sm","lg","fw","ul","li","border","pull-left","pull-right","spin","pulse","rotate-90","rotate-180","rotate-270","flip-horizontal","flip-vertical","stack","stack-1x","stack-2x","inverse","layers","layers-text","layers-counter"].concat(m.map(function(c){return c+"x"})).concat(a.map(function(c){return"w-"+c}));var s=v||{};s[z]||(s[z]={}),s[z].styles||(s[z].styles={}),s[z].hooks||(s[z].hooks={}),s[z].shims||(s[z].shims=[]);var t=s[z],f=Object.assign||function(c){for(var l=1;l>>0;h--;)l[h]=c[h];return l}function X(c){return c.classList?D(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Y(c,l){var h,v=l.split("-"),z=v[0],e=v.slice(1).join("-");return z!==c||""===e||(h=e,~d.indexOf(h))?null:e}function U(c){return(""+c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function K(h){return Object.keys(h||{}).reduce(function(c,l){return c+(l+": ")+h[l]+";"},"")}function G(c){return c.size!==I.size||c.x!==I.x||c.y!==I.y||c.rotate!==I.rotate||c.flipX||c.flipY}function J(c){var l=c.transform,h=c.containerWidth,v=c.iconWidth;return{outer:{transform:"translate("+h/2+" 256)"},inner:{transform:"translate("+32*l.x+", "+32*l.y+") "+" "+("scale("+l.size/16*(l.flipX?-1:1)+", "+l.size/16*(l.flipY?-1:1)+") ")+" "+("rotate("+l.rotate+" 0 0)")},path:{transform:"translate("+v/2*-1+" -256)"}}}var Q={x:0,y:0,width:"100%",height:"100%"},Z=function(c){var l=c.children,h=c.attributes,v=c.main,z=c.mask,e=c.transform,m=v.width,a=v.icon,s=z.width,t=z.icon,f=J({transform:e,containerWidth:s,iconWidth:m}),M={tag:"rect",attributes:S({},Q,{fill:"white"})},r={tag:"g",attributes:S({},f.inner),children:[{tag:"path",attributes:S({},a.attributes,f.path,{fill:"black"})}]},H={tag:"g",attributes:S({},f.outer),children:[r]},i="mask-"+B(),n="clip-"+B(),V={tag:"defs",children:[{tag:"clipPath",attributes:{id:n},children:[t]},{tag:"mask",attributes:S({},Q,{id:i,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[M,H]}]};return l.push(V,{tag:"rect",attributes:S({fill:"currentColor","clip-path":"url(#"+n+")",mask:"url(#"+i+")"},Q)}),{children:l,attributes:h}},$=function(c){var l=c.children,h=c.attributes,v=c.main,z=c.transform,e=K(c.styles);if(0"+m.map(uc).join("")+""+l+">"}var dc=function(){};function pc(c){return"string"==typeof(c.getAttribute?c.getAttribute(g):null)}var bc={replace:function(c){var l=c[0],h=c[1].map(function(c){return uc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+(O.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- "+l.outerHTML+" --\x3e":"");else if(l.parentNode){var v=document.createElement("span");l.parentNode.replaceChild(v,l),v.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~X(l).indexOf(O.replacementClass))return bc.replace(c);var v=new RegExp(O.familyPrefix+"-.*");delete h[0].attributes.style;var z=h[0].attributes.class.split(" ").reduce(function(c,l){return l===O.replacementClass||l.match(v)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=z.toSvg.join(" ");var e=h.map(function(c){return uc(c)}).join("\n");l.setAttribute("class",z.toNode.join(" ")),l.setAttribute(g,""),l.innerHTML=e}};function gc(h,c){var v="function"==typeof c?c:dc;0===h.length?v():(a.requestAnimationFrame||function(c){return c()})(function(){var c=!0===O.autoReplaceSvg?bc.replace:bc[O.autoReplaceSvg]||bc.replace,l=sc.begin("mutate");h.map(c),l(),v()})}var wc=!1;var yc=null;var Sc=function(c){var l=c.getAttribute("style"),h=[];return l&&(h=l.split(";").reduce(function(c,l){var h=l.split(":"),v=h[0],z=h.slice(1);return v&&0li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1em}.svg-inline--fa.fa-stack-2x{height:2em;width:2em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}";if("fa"!==l||h!==c){var z=new RegExp("\\.fa\\-","g"),e=new RegExp("\\."+c,"g");v=v.replace(z,"."+l+"-").replace(e,"."+h)}return v};var Qc=function(){function c(){w(this,c),this.definitions={}}return y(c,[{key:"add",value:function(){for(var l=this,c=arguments.length,h=Array(c),v=0;v {
+ render(): JSX.Element {
+ return (
+
+
{this.props.title}
+
+
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/client/src/components/CarContainer.tsx b/client/src/components/CarContainer.tsx
new file mode 100644
index 0000000..40725b6
--- /dev/null
+++ b/client/src/components/CarContainer.tsx
@@ -0,0 +1,128 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import * as Redux from "redux";
+import { addCar, deleteCar, updateCar } from "../data/actions";
+import { ApplicationState, Car } from "../data/models";
+import CarDetailsDialog from "./CarDetailsDialog";
+import CarEntryDialog from "./CarEntryDialog";
+import CarListItem from "./CarListItem";
+import CarRemoveDialog from "./CarRemoveDialog";
+
+// Properties to get from Redux Store
+interface StoreProps {
+ cars: Car[];
+}
+
+// Properties to get for Redux Dispatch
+interface DispatchProps {
+ insertCar: (car: Car) => void;
+ updateCar: (car: Car) => void;
+ deleteCar: (car: Car) => void;
+}
+
+// Properties external to Redux Store
+export interface OwnProps {
+ // No properties
+}
+
+// State inside (if stateful) Component
+interface OwnState {
+ selectedCar?: Car;
+}
+
+type CombinedProps = StoreProps & DispatchProps & OwnProps;
+
+class CarContainer extends React.Component {
+ constructor(props: CombinedProps) {
+ super(props);
+ this.state = { selectedCar: null };
+ }
+
+ handleCarSelected = (selectedCar: Car) => {
+ this.setState({ selectedCar });
+ };
+
+ render(): JSX.Element {
+ return (
+
+ {/* Table showing the contents */}
+
+ Add New
+
+
+
+
+ Manufacturer
+ Make
+ Model
+ Year
+ Edit
+ Delete
+
+
+
+ {this.props.cars.map(car => (
+
+ ))}
+ {this.props.children}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(
+ state: ApplicationState,
+ ownProps: OwnProps
+): StoreProps {
+ return {
+ cars: state.cars
+ };
+}
+
+function mapDispatchToProps(
+ dispatch: Redux.Dispatch,
+ ownProps: OwnProps
+): DispatchProps {
+ return {
+ insertCar: car => dispatch(addCar(car)),
+ updateCar: car => dispatch(updateCar(car)),
+ deleteCar: car => dispatch(deleteCar(car))
+ };
+}
+
+const wrapper = connect(
+ mapStateToProps,
+ mapDispatchToProps
+);
+
+export default wrapper(CarContainer);
diff --git a/client/src/components/CarDetailsDialog.tsx b/client/src/components/CarDetailsDialog.tsx
new file mode 100644
index 0000000..758f6b0
--- /dev/null
+++ b/client/src/components/CarDetailsDialog.tsx
@@ -0,0 +1,163 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import * as Redux from "redux";
+import { ApplicationState, Car } from "../data/models";
+
+// Properties to get from Redux Store
+interface StoreProps {
+ // No properties
+}
+
+// Properties to get for Redux Dispatch
+interface DispatchProps {
+ // No properties
+}
+
+// Properties external to Redux Store
+export interface OwnProps {
+ id: string;
+ car?: Car;
+}
+
+// State inside (if stateful) component
+interface OwnState {
+ // No properties
+}
+
+type CombinedProps = StoreProps & DispatchProps & OwnProps;
+
+class CarEntryDialog extends React.Component {
+ constructor(props: CombinedProps) {
+ super(props);
+ }
+
+ getTitle = () => "Car Details";
+ getSubtitle = () => "The car details are given below";
+
+ render(): JSX.Element {
+ return (
+
+
+
+
+
{this.getTitle()}
+
+ ×
+
+
+
+
{this.getSubtitle()}
+ {/* Input fields for each property should come here */}
+
+
+
+
+ OK
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(
+ state: ApplicationState,
+ ownProps: OwnProps
+): StoreProps {
+ return {
+ // No properties
+ };
+}
+
+function mapDispatchToProps(
+ dispatch: Redux.Dispatch,
+ ownProps: OwnProps
+): DispatchProps {
+ return {
+ // No properties
+ };
+}
+
+const wrapper = connect(
+ mapStateToProps,
+ mapDispatchToProps
+);
+
+export default wrapper(CarEntryDialog);
diff --git a/client/src/components/CarEntryDialog.tsx b/client/src/components/CarEntryDialog.tsx
new file mode 100644
index 0000000..9ffb535
--- /dev/null
+++ b/client/src/components/CarEntryDialog.tsx
@@ -0,0 +1,236 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import * as Redux from "redux";
+import { ApplicationState, Car } from "../data/models";
+
+// Properties to get from Redux Store
+interface StoreProps {
+ // No properties
+}
+
+// Properties to get for Redux Dispatch
+interface DispatchProps {
+ // No properties
+}
+
+// Properties external to Redux Store
+export interface OwnProps {
+ id: string;
+ mode: "insert" | "update";
+ car?: Car;
+ onSave: (car: Car) => void;
+}
+
+// State inside (if stateful) component
+interface OwnState {
+ newCar: Car;
+}
+
+type CombinedProps = StoreProps & DispatchProps & OwnProps;
+
+class CarEntryDialog extends React.Component {
+ constructor(props: CombinedProps) {
+ super(props);
+ // Initially set all variables to empty
+ this.state = {
+ newCar: this.props.car ? { ...this.props.car } : this.newCarState()
+ };
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ // Either set the modifying car's props or blank props as state, when component is updated
+ if (this.props != nextProps) {
+ if (nextProps.mode === "update" && nextProps.car) {
+ nextState.newCar = { ...nextProps.car };
+ } else if (nextProps.mode === "insert") {
+ nextState.newCar = this.newCarState();
+ }
+ }
+ }
+
+ newCarState = (): Car => {
+ return { id: 0, manufacturer: "", make: "", model: "", year: 2018 };
+ };
+
+ initState = () => {
+ this.setState({newCar : this.newCarState()});
+ }
+
+ handleEntry = event => {
+ let id = this.state.newCar.id;
+ let manufacturer = this.refs.manufacturer["value"];
+ let make = this.refs.make["value"];
+ let model = this.refs.model["value"];
+ let year = this.refs.year["value"];
+ let newCar = { id, manufacturer, make, model, year };
+ this.props.onSave({
+ ...this.state.newCar,
+ manufacturer,
+ make,
+ model,
+ year
+ });
+ this.initState();
+ };
+
+ getTitle = () =>
+ this.props.mode == "insert" ? "Add New Item" : "Update Item";
+ getSubtitle = () =>
+ this.props.mode == "insert"
+ ? "Enter item details and click on Save to Add."
+ : "Please update the details and click on Save.";
+
+ updateState = (event): void => {
+ let key: String = event.currentTarget.id;
+ let value: String = event.currentTarget.value;
+ switch (key) {
+ case "manufacturer": {
+ this.setState({
+ newCar: { ...this.state.newCar, manufacturer: value }
+ });
+ break;
+ }
+ case "make": {
+ this.setState({ newCar: { ...this.state.newCar, make: value } });
+ break;
+ }
+ case "model": {
+ this.setState({ newCar: { ...this.state.newCar, model: value } });
+ break;
+ }
+ case "year": {
+ this.setState({
+ newCar: { ...this.state.newCar, year: Number(value) }
+ });
+ break;
+ }
+ }
+ };
+
+ render(): JSX.Element {
+ return (
+
+
+
+
+
{this.getTitle()}
+ this.initState()}
+ aria-label="Close"
+ >
+ ×
+
+
+
+
{this.getSubtitle()}
+ {/* Input fields for each property should come here */}
+
+
+
+
+ Save
+
+ this.initState()}
+ data-dismiss="modal"
+ >
+ Close
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(
+ state: ApplicationState,
+ ownProps: OwnProps
+): StoreProps {
+ return {
+ // No properties
+ };
+}
+
+function mapDispatchToProps(
+ dispatch: Redux.Dispatch,
+ ownProps: OwnProps
+): DispatchProps {
+ return {
+ // No properties
+ };
+}
+
+const wrapper = connect(
+ mapStateToProps,
+ mapDispatchToProps
+);
+
+export default wrapper(CarEntryDialog);
diff --git a/client/src/components/CarListItem.tsx b/client/src/components/CarListItem.tsx
new file mode 100644
index 0000000..d87f6b5
--- /dev/null
+++ b/client/src/components/CarListItem.tsx
@@ -0,0 +1,117 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import * as Redux from "redux";
+import { deleteCar, updateCar } from "../data/actions";
+import { ApplicationState, Car } from "../data/models";
+
+// Properties to get from Redux Store
+interface StoreProps {
+ // No properties
+}
+
+// Properties to get for Redux Dispatch
+interface DispatchProps {
+ updateCar: (car: Car) => void;
+ deleteCar: (car: Car) => void;
+}
+
+// Properties external to Redux Store
+export interface OwnProps {
+ car: Car;
+ editTarget: String;
+ deleteTarget: String;
+ viewTarget: String;
+ notifySelected: (car: Car) => void;
+}
+
+// State inside (if stateful) component
+interface OwnState {
+ // No properties
+}
+
+type CombinedProps = StoreProps & DispatchProps & OwnProps;
+
+class CarListItem extends React.Component {
+ constructor(props: CombinedProps) {
+ super(props);
+ }
+
+ notifySelect = event => {
+ // Notify that this car was selected
+ this.props.notifySelected(this.props.car);
+ };
+
+ render(): JSX.Element {
+ const { manufacturer, make, model, year } = this.props.car;
+ return (
+
+
+
+
+ {manufacturer}
+
+
+
+
+ {make}
+
+
+ {model}
+
+
+ {year}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(
+ state: ApplicationState,
+ ownProps: OwnProps
+): StoreProps {
+ return {
+ // Nothing to map
+ };
+}
+
+function mapDispatchToProps(
+ dispatch: Redux.Dispatch,
+ ownProps: OwnProps
+): DispatchProps {
+ return {
+ updateCar: car => dispatch(updateCar(car)),
+ deleteCar: car => dispatch(deleteCar(car))
+ };
+}
+
+const wrapper = connect(
+ mapStateToProps,
+ mapDispatchToProps
+);
+
+export default wrapper(CarListItem);
diff --git a/client/src/components/CarRemoveDialog.tsx b/client/src/components/CarRemoveDialog.tsx
new file mode 100644
index 0000000..442fbc4
--- /dev/null
+++ b/client/src/components/CarRemoveDialog.tsx
@@ -0,0 +1,113 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import * as Redux from "redux";
+import { ApplicationState, Car } from "../data/models";
+
+// Properties to get from Redux Store
+interface StoreProps {
+ // No properties
+}
+
+// Properties to get for Redux Dispatch
+interface DispatchProps {
+ // No properties
+}
+
+// Properties external to Redux Store
+export interface OwnProps {
+ id: string;
+ car: Car;
+ onConfirm: (car: Car) => void;
+}
+
+// State inside (if stateful) component
+interface OwnState {
+ // No properties
+}
+
+type CombinedProps = StoreProps & DispatchProps & OwnProps;
+
+class CarRemoveDialog extends React.Component {
+ constructor(props: CombinedProps) {
+ super(props);
+ }
+
+ handleConfirm = event => {
+ this.props.onConfirm(this.props.car);
+ };
+
+ title = "Remove Item";
+ subtitle = "Are you sure you want to remove this item?";
+
+ render(): JSX.Element {
+ return (
+
+
+
+
+
{this.title}
+
+ ×
+
+
+
+
+
+ Yes
+
+
+ No
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(
+ state: ApplicationState,
+ ownProps: OwnProps
+): StoreProps {
+ return {
+ cars: state.cars
+ };
+}
+
+function mapDispatchToProps(
+ dispatch: Redux.Dispatch,
+ ownProps: OwnProps
+): DispatchProps {
+ return {
+ // No properties
+ };
+}
+
+const wrapper = connect(
+ mapStateToProps,
+ mapDispatchToProps
+);
+
+export default wrapper(CarRemoveDialog);
diff --git a/client/src/data/actions.tsx b/client/src/data/actions.tsx
new file mode 100644
index 0000000..72d7553
--- /dev/null
+++ b/client/src/data/actions.tsx
@@ -0,0 +1,34 @@
+import { ADD_CAR, DELETE_CAR, LOAD_INVENTORY, UPDATE_CAR } from "./constants";
+import { Car, StoreAction } from "./models";
+
+// Template Add New Car to Redux Store
+export const addCar = function(car: Car): StoreAction {
+ return {
+ type: ADD_CAR,
+ payload: car
+ };
+};
+
+// Template Update Existing Car in Redux Store
+export const updateCar = function(car: Car): StoreAction {
+ return {
+ type: UPDATE_CAR,
+ payload: car
+ };
+};
+
+// Template Delete Existing Car in Redux Store
+export const deleteCar = function(car: Car): StoreAction {
+ return {
+ type: DELETE_CAR,
+ payload: car
+ };
+};
+
+// Template that gets remote inventory and updates current
+export const loadInventory = function(cars: Car[]): StoreAction {
+ return {
+ type: LOAD_INVENTORY,
+ payload: cars
+ };
+};
diff --git a/client/src/data/constants.tsx b/client/src/data/constants.tsx
new file mode 100644
index 0000000..9bda57d
--- /dev/null
+++ b/client/src/data/constants.tsx
@@ -0,0 +1,8 @@
+// Updating local inventory
+export const ADD_CAR = "ADD_CAR";
+export const UPDATE_CAR = "UPDATE_CAR";
+export const DELETE_CAR = "DELETE_CAR";
+// Getting remote inventory
+export const LOAD_INVENTORY = "GET_INVENTORY";
+// URLS
+export const SERVER_URL = "http://localhost:5000";
diff --git a/client/src/data/models.tsx b/client/src/data/models.tsx
new file mode 100644
index 0000000..5435f2d
--- /dev/null
+++ b/client/src/data/models.tsx
@@ -0,0 +1,20 @@
+import { Action } from "redux";
+
+// Structure of Car
+export interface Car {
+ id: Number;
+ manufacturer: String;
+ make: String;
+ model: String;
+ year: Number;
+}
+
+// Structure of Application State
+export interface ApplicationState {
+ cars: Car[];
+}
+
+// Structure of Store Action
+export interface StoreAction extends Action {
+ payload?: V;
+}
diff --git a/client/src/data/req.tsx b/client/src/data/req.tsx
new file mode 100644
index 0000000..dc990d8
--- /dev/null
+++ b/client/src/data/req.tsx
@@ -0,0 +1,36 @@
+import * as Request from "request";
+import { SERVER_URL } from "./constants";
+import { Car } from "./models";
+import { init } from "./store";
+
+const options = (url: string, json) => {
+ return {
+ url: `${SERVER_URL}${url}`,
+ json
+ };
+};
+
+export const getInventory = (callback: (cars: Car[]) => void): void => {
+ Request.get(options("/api/car", true), (err, res, body) => {
+ if (err) {
+ callback([]);
+ } else {
+ callback(body);
+ }
+ });
+};
+
+export const addItem = (car: Car): void => {
+ console.log(car);
+ Request.post(options("/api/car", car), (err, res, body) => {
+ init();
+ });
+};
+
+export const updateItem = (car: Car): void => {
+ Request.put(options(`/api/car/${car.id}`, car));
+};
+
+export const removeItem = (car: Car): void => {
+ Request.delete(options(`/api/car/${car.id}`, true));
+};
diff --git a/client/src/data/store.tsx b/client/src/data/store.tsx
new file mode 100644
index 0000000..e397b8d
--- /dev/null
+++ b/client/src/data/store.tsx
@@ -0,0 +1,80 @@
+import { createStore, Store } from "redux";
+import { loadInventory } from "./actions";
+import { ADD_CAR, DELETE_CAR, LOAD_INVENTORY, UPDATE_CAR } from "./constants";
+import { ApplicationState, StoreAction } from "./models";
+import * as req from "./req";
+
+const initialState: ApplicationState = {
+ cars: []
+};
+
+// Root Reducer for all actions in the application
+function reduce(
+ state: ApplicationState = initialState,
+ action: StoreAction
+): ApplicationState {
+ let nextState = state;
+ switch (action.type) {
+ case ADD_CAR: {
+ // Return all cars plus the new one
+ nextState = {
+ ...state,
+ cars: [...state.cars, action.payload]
+ };
+ req.addItem(action.payload);
+ break;
+ }
+ case UPDATE_CAR: {
+ // Return all cars along with the updated (merged) car
+ let current = state.cars.filter(c => c.id === action.payload.id)[0];
+ if (current) {
+ let index = state.cars.indexOf(current);
+ let newCars = [...state.cars];
+ newCars[index] = action.payload;
+ nextState = {
+ ...state,
+ cars: newCars
+ };
+ req.updateItem(action.payload);
+ }
+ break;
+ }
+ case DELETE_CAR: {
+ // Return all cars except the deleted one
+ let current = state.cars.filter(c => c.id === action.payload.id)[0];
+ if (current) {
+ let index = state.cars.indexOf(current);
+ let newCars = [...state.cars];
+ newCars.splice(index, 1);
+ nextState = {
+ ...state,
+ cars: newCars
+ };
+ req.removeItem(action.payload);
+ }
+ break;
+ }
+ case LOAD_INVENTORY: {
+ // Reload inventory with new data
+ nextState = {
+ ...state,
+ cars: action.payload
+ };
+ break;
+ }
+ }
+ return nextState;
+}
+
+const store: Store> = createStore(
+ reduce
+);
+
+export const init = () => {
+ req.getInventory(cars => {
+ store.dispatch(loadInventory(cars));
+ });
+};
+
+init();
+export default store;
diff --git a/client/src/index.tsx b/client/src/index.tsx
new file mode 100644
index 0000000..35ba276
--- /dev/null
+++ b/client/src/index.tsx
@@ -0,0 +1,12 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import { App } from "./components/App";
+import Store from "./data/store";
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById("example")
+);
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..737a003
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "outDir": "./dist/",
+ "sourceMap": true,
+ "noImplicitAny": false,
+ "module": "commonjs",
+ "target": "es6",
+ "jsx": "react"
+ },
+ "include": [
+ "./src/**/*"
+ ],
+}
\ No newline at end of file
diff --git a/client/webpack.config.js b/client/webpack.config.js
new file mode 100644
index 0000000..590a774
--- /dev/null
+++ b/client/webpack.config.js
@@ -0,0 +1,55 @@
+module.exports = {
+
+ node: {
+ console: false,
+ global: true,
+ process: true,
+ __filename: 'mock',
+ __dirname: 'mock',
+ Buffer: true,
+ setImmediate: true,
+ fs : 'empty',
+ net : 'empty',
+ tls : 'empty'
+ },
+
+ entry: "./src/index.tsx",
+ output: {
+ filename: "bundle.js",
+ path: __dirname + "/dist"
+ },
+
+ // Enable sourcemaps for debugging webpack's output.
+ devtool: "source-map",
+
+ resolve: {
+ // Add '.ts' and '.tsx' as resolvable extensions.
+ extensions: [".ts", ".tsx", ".js", ".json"]
+ },
+
+ module: {
+ rules: [
+ // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
+ {
+ test: /\.tsx?$/,
+ loader: "awesome-typescript-loader"
+ },
+
+ // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
+ {
+ enforce: "pre",
+ test: /\.js$/,
+ loader: "source-map-loader"
+ }
+ ]
+ },
+
+ // When importing a module whose path matches one of the following, just
+ // assume a corresponding global variable exists and use that instead.
+ // This is important because it allows us to avoid bundling all of our
+ // dependencies, which allows browsers to cache those libraries between builds.
+ externals: {
+ "react": "React",
+ "react-dom": "ReactDOM"
+ },
+};
\ No newline at end of file