diff --git a/package-lock.json b/package-lock.json index 79f52b34f..5e66f5f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -434,6 +434,15 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" }, + "axios": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "requires": { + "follow-redirects": "1.5.0", + "is-buffer": "1.1.6" + } + }, "axobject-query": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", @@ -2207,6 +2216,15 @@ "sha.js": "2.4.11" } }, + "create-react-context": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz", + "integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==", + "requires": { + "fbjs": "0.8.17", + "gud": "1.0.0" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3828,6 +3846,11 @@ } } }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4500,6 +4523,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", @@ -4657,6 +4685,18 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" }, + "history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "requires": { + "invariant": "2.2.4", + "loose-envify": "1.3.1", + "resolve-pathname": "2.2.0", + "value-equal": "0.4.0", + "warning": "3.0.0" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -4667,6 +4707,11 @@ "minimalistic-crypto-utils": "1.0.1" } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -8809,6 +8854,32 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.0.tgz", "integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==" }, + "react-router": { + "version": "4.4.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.4.0-alpha.0.tgz", + "integrity": "sha512-R/8t+mZCF9Wy/AUpMkBLpD38WuBLGVeZ/5vdOcmTzntH7Krxum89sQ8ThvzOjXvWnq9speuwPf9Fq+13NslXIw==", + "requires": { + "create-react-context": "0.2.2", + "history": "4.7.2", + "hoist-non-react-statics": "2.5.5", + "invariant": "2.2.4", + "loose-envify": "1.3.1", + "path-to-regexp": "1.7.0", + "prop-types": "15.6.1" + } + }, + "react-router-dom": { + "version": "4.4.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.4.0-alpha.0.tgz", + "integrity": "sha512-gGUnDmfhKp9dCgHb5rZ/p43ClEr3k41LT9z3cyFGJQDqe6rhxWHwPU4b9lzbIl8BykCf2+YCq3lL/nAtydh3Lw==", + "requires": { + "history": "4.7.2", + "invariant": "2.2.4", + "loose-envify": "1.3.1", + "prop-types": "15.6.1", + "react-router": "4.4.0-alpha.0" + } + }, "react-scripts": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-1.1.4.tgz", @@ -9231,6 +9302,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -10564,6 +10640,11 @@ "spdx-expression-parse": "3.0.0" } }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -10600,6 +10681,14 @@ "makeerror": "1.0.11" } }, + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "1.3.1" + } + }, "watch": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", diff --git a/package.json b/package.json index 951a38e2f..6833379b9 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^0.18.0", + "font-awesome": "^4.7.0", "react": "^16.4.1", "react-dom": "^16.4.1", + "react-router-dom": "^4.4.0-alpha.0", "react-scripts": "1.1.4" }, "scripts": { @@ -13,4 +16,4 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" } -} \ No newline at end of file +} diff --git a/public/favicon.ico b/public/favicon.ico index a11777cc4..59bb3170c 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/App.css b/src/App.css index c5c6e8a68..6f101396a 100644 --- a/src/App.css +++ b/src/App.css @@ -2,27 +2,54 @@ text-align: center; } -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 80px; +body { + background: steelblue; + background-repeat: repeat; } -.App-header { +header { background-color: #222; - height: 150px; padding: 20px; color: white; + height: 60px; + line-height: 60px; + display: flex; + flex-wrap: wrap; + justify-content: stretch; } -.App-title { - font-size: 1.5em; +header p { + margin: 0; } -.App-intro { - font-size: large; +header a { + color: #fff; + font-size: 1.25em; } -@keyframes App-logo-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +button.col-1 { + margin: .5em; + display: inline-block; + font-size: 1.25em; + cursor: pointer; + flex: none; +} + +.search { + margin: .5em; + display: inline-block; + font-size: 1.25em; + cursor: pointer; + flex: none; +} + +.col-1 { + flex-grow: 1; +} + +.tagline { + text-align: center; + color: white; + font-weight: bold; + text-shadow: 2px 2px 4px #000000; } diff --git a/src/App.js b/src/App.js index 203067e4d..accd69f8d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,19 +1,125 @@ import React, { Component } from 'react'; -import logo from './logo.svg'; +import { BrowserRouter as Router, Link, Route } from 'react-router-dom'; +import axios from 'axios'; +import Library from './components/Library'; +import Search from './components/Search'; +import CustomerList from './components/CustomerList'; +import Status from './components/Status'; + import './App.css'; class App extends Component { + constructor(){ + super(); + this.state = { + customerName: "", + customerId: 0, + movieTitle: "", + movieId: 0, + status: { + message: "", + type: "" + } + } + } + + displayCurrentCustomer = (name) => { + this.setState({ + customerName: name + }); + } + + updateCurrentCustomer = (id) => { + this.setState({ + customerId: id + }); + } + + updateCurrentMovie = (id) => { + this.setState({ + movieId: id + }); + } + + displayCurrentMovie = (title) => { + this.setState({ + movieTitle: title + }); + } + + updateStatus = (message, type) => { + const updatedStatus = { + message: message, + type: type + }; + this.setState({ + status: updatedStatus + }); + } + + createNewRental = () => { + this.updateStatus('Creating new rental', 'success'); + let dueDate = new Date(); + dueDate.setDate(dueDate.getDate() + 7); + dueDate = `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, "0")}-${String(dueDate.getDate()).padStart(2, "0")}`; + + const NEW_RENTAL_URL = `http://localhost:3000/rentals/${this.state.movieTitle}/check-out`; + axios.post(NEW_RENTAL_URL, { + due_date: dueDate, + customer_id: this.state.customerId + }) + .then(() => { + this.updateStatus(`Successfully rented ${this.state.movieTitle}`, 'success'); + }) + .catch((error) => { + this.updateStatus(`Encountered an error when checking out ${this.state.movieTitle}: ${error.message}`, 'error'); + }); + } + render() { return ( -
-
- logo -

Welcome to React

+ +
+
+ Search for Movies + All Movies + Customers + +

Current Customer: {this.state.customerName}

+

Current Movie: {this.state.movieTitle}

+
-

- To get started, edit src/App.js and save to reload. -

-
+ +

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

+

Like Netflix, but not as Convenient

+

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

+ + } + /> + } + /> + } + /> +
+ ); } } diff --git a/src/backgroundImage.jpg b/src/backgroundImage.jpg new file mode 100644 index 000000000..660811fb3 Binary files /dev/null and b/src/backgroundImage.jpg differ diff --git a/src/components/CustomerList.css b/src/components/CustomerList.css new file mode 100644 index 000000000..aad5ec638 --- /dev/null +++ b/src/components/CustomerList.css @@ -0,0 +1,40 @@ +.customerContainer { + display: flex; + flex-wrap: wrap; + padding: 1em; + margin: 1em auto; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.customerContent { + width: 50%; + flex-direction: column; + margin: 0 auto; +} + +.customer { + font-size: 2em; + font-weight: bold; + display: inline-block; + width: 500px; + height: 100px; + line-height: 100px; + margin: 16px auto; + border: 1px solid black; + padding: 16px; + text-align: center; + margin-bottom: 0; + background-color: honeydew; +} + +.selectCustomer { + font-size: 2em; + background-color: #222; + width: 534px; + color: white; + cursor: pointer; + margin: 0 auto; + border: none; +} diff --git a/src/components/CustomerList.js b/src/components/CustomerList.js new file mode 100644 index 000000000..a6a3a551d --- /dev/null +++ b/src/components/CustomerList.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import axios from 'axios'; +import PropTypes from 'prop-types'; + +import NewCustomerForm from './NewCustomerForm'; +import './CustomerList.css' + +class CustomerList extends Component { + constructor() { + super(); + this.state = { + customers: [], + displayNewCustomerForm: false + } + } + + onCustomerSelect = (event) => { + this.props.displayCurrentCustomerCallback(event.target.name); + this.props.updateCurrentCustomerCallback(event.target.value); + } + + componentDidMount() { + this.props.updateStatusCallback('Loading customers...', 'success'); + const CUSTOMERS_URL = 'http://localhost:3000' + '/customers'; + axios.get(CUSTOMERS_URL) + .then((response) => { + let updatedCustomers = []; response.data.map((customer) => { + updatedCustomers.push(customer); + }); + this.props.updateStatusCallback(`${updatedCustomers.length} customers successfully loaded`, 'success'); + this.setState({ + customers: updatedCustomers + }); + }) + .catch((error) => { + this.props.updateStatusCallback(`Something went wrong: ${error.message}`, 'error'); + }); + } + + toggleNewCustomerForm = () => { + this.setState({ + displayNewCustomerForm: !this.state.displayNewCustomerForm + }); + } + + render() { + + let newCustomerForm = null; + if (this.state.displayNewCustomerForm) { + newCustomerForm = +
+

New Customer!

+ +
; + } + + + const customers = this.state.customers.map((customer) => { + return ( +
+

{customer.name}

+ +
+ ); + }); + + return ( +
+

CustomerList!

+ + {newCustomerForm} + {customers} +
+ ); + } +} + +CustomerList.propTypes = { + updateCurrentCustomerCallback: PropTypes.func.isRequired, + displayCurrentCustomerCallback: PropTypes.func.isRequired, + updateStatusCallback: PropTypes.func.isRequired +}; + +export default CustomerList; diff --git a/src/components/Library.css b/src/components/Library.css new file mode 100644 index 000000000..2e9fc7779 --- /dev/null +++ b/src/components/Library.css @@ -0,0 +1,39 @@ +.libraryContainer { + display: flex; + flex-wrap: wrap; + padding: 1em; + margin: 1em auto; + justify-content: space-around; +} + +.libraryContent { + width: 200px; + height: 400px; + display: flex; + flex-direction: column; + align-items: space-around; + margin: 1em; +} + +.movieImage { + border: solid white 4px; + box-shadow: 5px 10px gainsboro; +} + +.movieImage:hover { + transition: .5s ease-in-out; + transform: scale(1.1); +} + +.movieTitle { + padding-top: 1.5em; + text-align: center; + font-size: 15px; + font-weight: bold; +} + +.selectMovie { + padding-bottom: .5em; + font-size: 1.25em; + cursor: pointer; +} diff --git a/src/components/Library.js b/src/components/Library.js new file mode 100644 index 000000000..ccb2ed898 --- /dev/null +++ b/src/components/Library.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import axios from 'axios'; + +import './Library.css'; + +class Library extends Component { + constructor() { + super(); + this.state = { + movies: [] + } + } + + onMovieSelect = (event) => { + this.props.displayCurrentMovieCallback(event.target.name); + this.props.updateCurrentMovieCallback(event.target.value); + } + + componentDidMount() { + const LIBRARY_URL = 'http://localhost:3000' + '/movies'; + this.props.updateStatusCallback('loading movies...', 'success'); + axios.get(LIBRARY_URL) + .then((response) => { + let updatedMovies = [] + response.data.map((movie) => { + updatedMovies.push(movie); + }); + this.props.updateStatusCallback(`${updatedMovies.length} movies successfully loaded`, 'success') + this.setState({ + movies: updatedMovies + }); + }) + .catch((error) => { + this.props.updateStatusCallback(`Something went wrong: ${error.message}`, 'error') + }); + } + + render() { + const movies = this.state.movies.map((movie) => { + return ( +
+
+ +

{movie.title}

+ +
+
+ ); + }); + return ( +
+ {movies} +
+ ); + } +} + +Library.propTypes = { + updateCurrentMovieCallback: PropTypes.func.isRequired, + displayCurrentMovieCallback: PropTypes.func.isRequired, + updateStatusCallback: PropTypes.func.isRequired +} + +export default Library; diff --git a/src/components/NewCustomerForm.css b/src/components/NewCustomerForm.css new file mode 100644 index 000000000..37c22ff0b --- /dev/null +++ b/src/components/NewCustomerForm.css @@ -0,0 +1,20 @@ +form { + width: 50%; + margin: 0 auto; + padding: 0; +} + +div { + text-align: center; +} + +label { + display: inline-block; + width: 8em; + font-size: 1.5em; +} + +input { + display: inline-block; + width: 20em; +} diff --git a/src/components/NewCustomerForm.js b/src/components/NewCustomerForm.js new file mode 100644 index 000000000..47fc9de1e --- /dev/null +++ b/src/components/NewCustomerForm.js @@ -0,0 +1,124 @@ +import React from 'react'; +import axios from 'axios'; + +import './Search.css'; +import './NewCustomerForm.css'; + +class NewCustomerForm extends React.Component { + constructor() { + super(); + this.state = { + name: '', + address: '', + city: '', + state: '', + postal_code: '', + phone: '', + account_credit: '' + } + } + + onInputChange = (event) => { + const updatedState = {} + updatedState[event.target.name] = event.target.value; + this.setState(updatedState); + } + + onNewCustomerFormSubmit = (event) => { + event.preventDefault(); + const NEW_CUSTOMER_URL = 'http://localhost:3000/customers/'; + const newCustomer = this.state; + console.log(typeof newCustomer.accountCredit); + axios.post(NEW_CUSTOMER_URL, this.state) + .then((response) => { + console.log(response); + }) + .catch((error) => { + console.log(error); + }); + this.setState({ + name: '', + address: '', + city: '', + state: '', + postal_code: '', + phone: '', + account_credit: '' + }); + } + + render () { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ ); + } +} + +export default NewCustomerForm; diff --git a/src/components/Search.css b/src/components/Search.css new file mode 100644 index 000000000..a2d0ccd22 --- /dev/null +++ b/src/components/Search.css @@ -0,0 +1,20 @@ +section { + padding: 2em; + text-align: center; +} + +.searchInput { + margin: 1em; + font-size: 1em; + padding: .75em; + width: 250px; +} + +.searchBtn { + margin: 1em; + color: white; + background-color: black; + cursor: pointer; + font-size: 1.5em; + padding: .75em; +} diff --git a/src/components/Search.js b/src/components/Search.js new file mode 100644 index 000000000..1f1e2bbcc --- /dev/null +++ b/src/components/Search.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import axios from 'axios'; + +import './Search.css' + +class Search extends Component { + constructor() { + super(); + this.state = { + searchTerm: "", + results: [], + } + } + + onInputChange = (event) => { + let updatedSearchTerm = {} + updatedSearchTerm['searchTerm'] = event.target.value; + this.setState(updatedSearchTerm); + } + + onFormSubmit = (event) => { + event.preventDefault(); + this.props.updateStatusCallback('Searching for movies...', 'success'); + const SEARCH_URL = `http://localhost:3000/movies?query=${this.state.searchTerm}`; + axios.get(SEARCH_URL) + .then((response) => { + let updatedResults = []; + response.data.map((result) => { + updatedResults.push(result) + }); + + if(updatedResults.length === 0) { + this.props.updateStatusCallback('Sorry, no movies were found', 'success'); + } else { + this.props.updateStatusCallback(`${updatedResults.length} movies were found`, 'success'); + } + + this.setState({ + results: updatedResults, + searchTerm: "" + }); + }) + .catch((error) => { + this.props.updateStatusCallback(`Something went wrong: ${error.message}`, 'error'); + }); + } + + addToLibrary = (movie) => { + this.props.updateStatusCallback(`Adding ${movie.title} to the library`, 'success'); + const NEW_MOVIE_URL = 'http://localhost:3000/movies'; + let image = movie.image_url.split('w185'); + image = image[1]; + + axios.post(NEW_MOVIE_URL, { + title: movie.title, + overview: movie.overview, + release_date: movie.release_date, + image_url: image, + external_id: 2 + }) + .then(() => { + this.props.updateStatusCallback(`Successfully added ${movie.title} to the library`, 'success'); + }) + .catch((error) => { + this.props.updateStatusCallback(`Something went wrong: ${error.message}`, 'error'); + }); + } + + render() { + console.log(this.state.searchTerm); + const results = this.state.results.map((movie) => { + return( +
+
+

+ +

{movie.title}

+ +

+
+
+ ); + }); + + return ( +
+
+ + +
+ +
+ {results} +
+ +
+ ); + } +} + +Search.propTypes = { + updateStatusCallback: PropTypes.func.isRequired +} + +export default Search; diff --git a/src/components/Status.css b/src/components/Status.css new file mode 100644 index 000000000..68c496522 --- /dev/null +++ b/src/components/Status.css @@ -0,0 +1,15 @@ +.success { + background-color: #98FB98; + padding: 1em; + margin: .75em; + border: 1px solid #006400; + color: #006400; +} + +.error { + background-color: #FF6347; + padding: 1em; + margin: .75em; + border: 1px solid #800000; + color: #800000; +} diff --git a/src/components/Status.js b/src/components/Status.js new file mode 100644 index 000000000..7ca092fce --- /dev/null +++ b/src/components/Status.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './Status.css'; + +const Status = (props) => { + return( +

{props.message}

+ ); +} + +Status.propTypes = { + type: PropTypes.string.isRequired, + message: PropTypes.string.isRequired +} + +export default Status; diff --git a/src/index.js b/src/index.js index fae3e3500..dfd2d0e75 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; +import '../node_modules/font-awesome/css/font-awesome.min.css'; ReactDOM.render(, document.getElementById('root')); registerServiceWorker(); diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 6b60c1042..000000000 --- a/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - -