├── .gitignore
├── .vscode
└── launch.json
├── README.md
├── counter-app
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── counter.jsx
│ │ ├── counters.jsx
│ │ └── navbar.jsx
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ └── registerServiceWorker.js
└── yarn.lock
├── http-app
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── config.json
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── registerServiceWorker.js
│ └── services
│ │ ├── httpService.js
│ │ └── logService.js
└── yarn.lock
├── mosh-react.code-workspace
├── react-app
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ └── index.js
└── yarn.lock
├── router-app
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── admin
│ │ │ ├── dashboard.jsx
│ │ │ ├── posts.jsx
│ │ │ ├── sidebar.jsx
│ │ │ └── users.jsx
│ │ ├── home.jsx
│ │ ├── navbar.jsx
│ │ ├── notFound.jsx
│ │ ├── posts.jsx
│ │ ├── productDetails.jsx
│ │ └── products.jsx
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ └── registerServiceWorker.js
└── yarn.lock
├── vidly-api-node
├── .gitignore
├── config
│ ├── custom-environment-variables.json
│ ├── default.json
│ └── test.json
├── index.js
├── middleware
│ ├── admin.js
│ ├── async.js
│ ├── auth.js
│ ├── error.js
│ ├── validate.js
│ └── validateObjectId.js
├── models
│ ├── customer.js
│ ├── genre.js
│ ├── movie.js
│ ├── rental.js
│ └── user.js
├── package.json
├── readme.md
├── routes
│ ├── auth.js
│ ├── customers.js
│ ├── genres.js
│ ├── movies.js
│ ├── rentals.js
│ ├── returns.js
│ └── users.js
├── seed.js
├── startup
│ ├── config.js
│ ├── cors.js
│ ├── db.js
│ ├── logging.js
│ ├── routes.js
│ └── validation.js
├── tests
│ ├── integration
│ │ ├── auth.test.js
│ │ ├── genres.test.js
│ │ └── returns.test.js
│ └── unit
│ │ ├── middleware
│ │ └── auth.test.js
│ │ └── models
│ │ └── user.test.js
├── uncaughtExceptions.log
└── yarn.lock
└── vidly
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── components
│ ├── common
│ │ ├── form.jsx
│ │ ├── input.jsx
│ │ ├── like.jsx
│ │ ├── listGroup.jsx
│ │ ├── pagination.jsx
│ │ ├── protectedRoute.jsx
│ │ ├── select.jsx
│ │ ├── table.jsx
│ │ ├── tableBody.jsx
│ │ └── tableHeader.jsx
│ ├── customers.jsx
│ ├── loginForm.jsx
│ ├── logout.jsx
│ ├── movieForm.jsx
│ ├── movies.jsx
│ ├── moviesTable.jsx
│ ├── navbar.jsx
│ ├── notFound.jsx
│ ├── registerForm.jsx
│ ├── rentals.jsx
│ └── searchBox.jsx
├── config.json
├── index.css
├── index.js
├── logo.svg
├── registerServiceWorker.js
├── services
│ ├── authService.js
│ ├── genreService.js
│ ├── httpService.js
│ ├── logService.js
│ ├── movieService.js
│ └── userService.js
└── utils
│ └── paginate.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:3000",
12 | "webRoot": "${workspaceFolder}"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mastering-react-mosh
--------------------------------------------------------------------------------
/counter-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/counter-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "4.1.1",
7 | "react": "^16.4.2",
8 | "react-dom": "^16.4.2",
9 | "react-scripts": "1.1.5"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test --env=jsdom",
15 | "eject": "react-scripts eject"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/counter-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fishstick22/mastering-react-mosh/54b01a22f951e8930c9a9c7bcc8db55a01d809db/counter-app/public/favicon.ico
--------------------------------------------------------------------------------
/counter-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/counter-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/counter-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/counter-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./App.css";
3 | import NavBar from "./components/navbar";
4 | import Counters from "./components/counters";
5 |
6 | class App extends Component {
7 | state = {
8 | counters: [
9 | { id: 1, value: 0 },
10 | { id: 2, value: 0 },
11 | { id: 3, value: 0 },
12 | { id: 4, value: 0 }
13 | ]
14 | };
15 |
16 | handleIncrement = counter => {
17 | console.log("Increment", counter);
18 | const counters = [...this.state.counters];
19 | const index = counters.indexOf(counter);
20 | counters[index] = { ...counter };
21 | counters[index].value++;
22 | this.setState({ counters });
23 | };
24 |
25 | handleDecrement = counter => {
26 | console.log("Decrement", counter);
27 | const counters = [...this.state.counters];
28 | const index = counters.indexOf(counter);
29 | counters[index] = { ...counter };
30 | counters[index].value--;
31 | this.setState({ counters });
32 | };
33 |
34 | handleReset = () => {
35 | const counters = this.state.counters.map(c => {
36 | c.value = 0;
37 | return c;
38 | });
39 | this.setState({ counters });
40 | };
41 |
42 | handleDelete = counterId => {
43 | console.log("Event Handler Called", counterId);
44 | const counters = this.state.counters.filter(c => c.id !== counterId);
45 | this.setState({ counters });
46 | };
47 |
48 | render() {
49 | return (
50 |
51 | c.value > 0).length} />
52 |
53 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/counter-app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/counter-app/src/components/counter.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | class Counter extends Component {
4 | render() {
5 | console.log("props", this.props);
6 | const { counter, onIncrement, onDecrement, onDelete } = this.props;
7 | return (
8 |
9 |
10 | {this.formatCount()}
11 |
12 |
13 |
19 |
26 |
32 |
33 |
34 | );
35 | }
36 |
37 | getBadgeClasses() {
38 | let classes = "badge m-2 badge-";
39 | classes += this.props.counter.value === 0 ? "warning" : "primary";
40 | return classes;
41 | }
42 |
43 | formatCount() {
44 | const { value } = this.props.counter;
45 | return value === 0 ? "Zero" : value;
46 | }
47 | }
48 |
49 | export default Counter;
50 |
--------------------------------------------------------------------------------
/counter-app/src/components/counters.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Counter from "./counter";
3 |
4 | class Counters extends Component {
5 | render() {
6 | const {counters, onReset, onDelete, onIncrement, onDecrement} = this.props;
7 | return (
8 |
9 |
15 | {counters.map(counter => (
16 |
23 | ))}
24 |
25 | );
26 | }
27 | }
28 |
29 | export default Counters;
30 |
--------------------------------------------------------------------------------
/counter-app/src/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Stateless Functional Component
4 | const NavBar = ({totalCounters}) => {
5 | return (
6 |
14 | );
15 | };
16 |
17 | export default NavBar;
18 |
--------------------------------------------------------------------------------
/counter-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/counter-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 | import 'bootstrap/dist/css/bootstrap.css';
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 | registerServiceWorker();
10 |
--------------------------------------------------------------------------------
/counter-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/counter-app/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/http-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/http-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "bootstrap": "^4.1.2",
8 | "react": "^16.4.1",
9 | "react-dom": "^16.4.1",
10 | "react-scripts": "1.1.4",
11 | "react-toastify": "^4.1.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test --env=jsdom",
17 | "eject": "react-scripts eject"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/http-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fishstick22/mastering-react-mosh/54b01a22f951e8930c9a9c7bcc8db55a01d809db/http-app/public/favicon.ico
--------------------------------------------------------------------------------
/http-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/http-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/http-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/http-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | // import axios from "axios";
3 | import { ToastContainer } from "react-toastify";
4 | import http from "./services/httpService";
5 | import config from "./config.json";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import "./App.css";
8 |
9 | class App extends Component {
10 | state = {
11 | posts: []
12 | };
13 |
14 | async componentDidMount() {
15 | // pending > resolved (success) OR rejected (failure)
16 | // const { data: posts } = await axios.get(apiEndpoint);
17 | const { data: posts } = await http.get(config.apiEndpoint);
18 | this.setState({ posts });
19 | }
20 |
21 | handleAdd = async () => {
22 | const obj = { title: "a", body: "b" };
23 | // const { data: post } = await axios.post(apiEndpoint, obj);
24 | const { data: post } = await http.post(config.apiEndpoint, obj);
25 |
26 | const posts = [post, ...this.state.posts];
27 | this.setState({ posts })
28 | };
29 |
30 | handleUpdate = async post => {
31 | const originalPost = { ...post };
32 | post.title = "UPDATED";
33 |
34 | const posts = [...this.state.posts];
35 | const index = posts.indexOf(post);
36 | posts[index] = { ...post };
37 | this.setState({ posts });
38 |
39 | try {
40 | // await axios.put(apiEndpoint + "/" + post.id, post);
41 | await http.put(config.apiEndpoint + "/" + post.id, post);
42 | } catch (ex) {
43 | console.log('Reverting failed update for post:' + post.id);
44 | posts[index] = { ...originalPost };
45 | this.setState({ posts });
46 | }
47 | };
48 |
49 | handleDelete = async post => {
50 | const originalPosts = this.state.posts;
51 |
52 | const posts = this.state.posts.filter(p => p.id !== post.id);
53 | this.setState({ posts });
54 |
55 | try {
56 | // await axios.delete(apiEndpoint + "/" + post.id);
57 | await http.delete(config.apiEndpoint + "/" + post.id);
58 | } catch (ex) {
59 | if (ex.response && ex.response.status === 404)
60 | alert("This post has already been deleted.");
61 | this.setState({ posts: originalPosts });
62 | }
63 | };
64 |
65 | render() {
66 | return (
67 |
68 |
69 |
72 |
73 |
74 |
75 | Title |
76 | Update |
77 | Delete |
78 |
79 |
80 |
81 | {this.state.posts.map(post => (
82 |
83 | {post.title} |
84 |
85 |
91 | |
92 |
93 |
99 | |
100 |
101 | ))}
102 |
103 |
104 |
105 | );
106 | }
107 | }
108 |
109 | export default App;
110 |
--------------------------------------------------------------------------------
/http-app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/http-app/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiEndpoint": "https://jsonplaceholder.typicode.com/posts"
3 |
4 | }
--------------------------------------------------------------------------------
/http-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 30px;
4 | font-family: sans-serif;
5 | }
--------------------------------------------------------------------------------
/http-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import registerServiceWorker from "./registerServiceWorker";
5 | import logger from "./services/logService";
6 | import "./index.css";
7 | import "bootstrap/dist/css/bootstrap.css";
8 |
9 | logger.init();
10 |
11 | ReactDOM.render(, document.getElementById("root"));
12 | registerServiceWorker();
13 |
--------------------------------------------------------------------------------
/http-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/http-app/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/http-app/src/services/httpService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import logger from "./logService";
3 | import { toast } from "react-toastify";
4 |
5 | axios.interceptors.response.use(null, error => {
6 | const expectedError =
7 | error.response &&
8 | error.response.status >= 400 &&
9 | error.response.status < 500;
10 |
11 | if (!expectedError) {
12 | // console.log("logging the error", error);
13 | // alert("An unexpected error occurred.");
14 | logger.log(error);
15 | toast.error("An unexpected error occurred.");
16 | }
17 |
18 | return Promise.reject(error);
19 | });
20 |
21 | export default {
22 | get: axios.get,
23 | post: axios.post,
24 | put: axios.put,
25 | delete: axios.delete
26 | };
--------------------------------------------------------------------------------
/http-app/src/services/logService.js:
--------------------------------------------------------------------------------
1 | // import Raven from "raven-js";
2 |
3 | function init() {
4 | // Raven.config("ADD YOUR OWN API KEY", {
5 | // release: "1-0-0",
6 | // environment: "development-test"
7 | // }).install();
8 | }
9 |
10 | function log(error) {
11 | // Raven.captureException(error);
12 | }
13 |
14 | export default {
15 | init,
16 | log
17 | };
--------------------------------------------------------------------------------
/mosh-react.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "counter-app"
5 | },
6 | {
7 | "path": "react-app"
8 | },
9 | {
10 | "path": "vidly"
11 | },
12 | {
13 | "path": "router-app"
14 | },
15 | {
16 | "path": "http-app"
17 | },
18 | {
19 | "path": "vidly-api-node"
20 | }
21 | ],
22 | "settings": {}
23 | }
--------------------------------------------------------------------------------
/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.4.2",
7 | "react-dom": "^16.4.2",
8 | "react-scripts": "1.1.4"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test --env=jsdom",
14 | "eject": "react-scripts eject"
15 | }
16 | }
--------------------------------------------------------------------------------
/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fishstick22/mastering-react-mosh/54b01a22f951e8930c9a9c7bcc8db55a01d809db/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/react-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/react-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | const element = Hello World!
5 | console.log(element);
6 | ReactDOM.render(element, document.getElementById('root'));
--------------------------------------------------------------------------------
/router-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/router-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "router-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "query-string": "6.1.0",
7 | "react": "^16.4.1",
8 | "react-dom": "^16.4.1",
9 | "react-router-dom": "^4.3.1",
10 | "react-scripts": "1.1.4"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "test": "react-scripts test --env=jsdom",
16 | "eject": "react-scripts eject"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/router-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fishstick22/mastering-react-mosh/54b01a22f951e8930c9a9c7bcc8db55a01d809db/router-app/public/favicon.ico
--------------------------------------------------------------------------------
/router-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/router-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/router-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/router-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, Switch, Redirect } from "react-router-dom";
3 | import NavBar from "./components/navbar";
4 | import Products from "./components/products";
5 | import Posts from "./components/posts";
6 | import Home from "./components/home";
7 | import Dashboard from "./components/admin/dashboard";
8 | import ProductDetails from "./components/productDetails";
9 | import NotFound from "./components/notFound";
10 | import "./App.css";
11 |
12 | class App extends Component {
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | }
23 | />
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | export default App;
38 |
--------------------------------------------------------------------------------
/router-app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/router-app/src/components/admin/dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route } from "react-router-dom";
3 | import Sidebar from './sidebar';
4 | import Users from './users';
5 | import Posts from './posts';
6 |
7 | const Dashboard = ({ match }) => {
8 | return (
9 |
10 |
Admin Dashboard
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Dashboard;
20 |
--------------------------------------------------------------------------------
/router-app/src/components/admin/posts.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Posts = () => {
4 | return (
5 |
6 |
Admin Posts
7 |
8 | );
9 | };
10 |
11 | export default Posts;
12 |
--------------------------------------------------------------------------------
/router-app/src/components/admin/sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from 'react-router-dom';
3 |
4 | const Sidebar = () => {
5 | return (
6 |
7 | -
8 | Posts
9 |
10 | -
11 | Users
12 |
13 |
14 | );
15 | };
16 |
17 | export default Sidebar;
18 |
--------------------------------------------------------------------------------
/router-app/src/components/admin/users.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Users = () => {
4 | return Admin Users
;
5 | };
6 |
7 | export default Users;
8 |
--------------------------------------------------------------------------------
/router-app/src/components/home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Home = () => {
4 | return Home
;
5 | };
6 |
7 | export default Home;
8 |
--------------------------------------------------------------------------------
/router-app/src/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 |
5 | const NavBar = () => {
6 | return (
7 |
8 | -
9 | Home
10 |
11 | -
12 | Products
13 |
14 | -
15 | Posts
16 |
17 | -
18 | Admin
19 |
20 |
21 | );
22 | };
23 |
24 | export default NavBar;
25 |
--------------------------------------------------------------------------------
/router-app/src/components/notFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotFound = () => {
4 | return Not Found
;
5 | };
6 |
7 | export default NotFound;
8 |
--------------------------------------------------------------------------------
/router-app/src/components/posts.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import queryString from 'query-string';
3 |
4 | const Posts = ({ match, location }) => {
5 | const { sortBy} = queryString.parse(location.search);
6 | console.log(queryString.parse(location.search));
7 |
8 | return (
9 |
10 |
Posts
11 | Year: {match.params.year}, Month: {match.params.month}
12 |
13 | );
14 | };
15 |
16 | export default Posts;
17 |
--------------------------------------------------------------------------------
/router-app/src/components/productDetails.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | class ProductDetails extends Component {
4 | handleSave = () => {
5 | // Navigate to /products
6 | //this.props.history.push("/products");
7 | this.props.history.replace("/products");
8 | };
9 |
10 | render() {
11 | return (
12 |
13 |
Product Details - {this.props.match.params.id}
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default ProductDetails;
21 |
--------------------------------------------------------------------------------
/router-app/src/components/products.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | class Products extends Component {
5 | state = {
6 | products: [
7 | { id: 1, name: "Product 1" },
8 | { id: 2, name: "Product 2" },
9 | { id: 3, name: "Product 3" }
10 | ]
11 | };
12 |
13 | render() {
14 | return (
15 |
16 |
Products
17 |
18 | {this.state.products.map(product => (
19 | -
20 | {product.name}
21 |
22 | ))}
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | export default Products;
30 |
--------------------------------------------------------------------------------
/router-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 30px;
4 | font-family: sans-serif;
5 | }
--------------------------------------------------------------------------------
/router-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import './index.css';
5 | import App from './App';
6 | import registerServiceWorker from './registerServiceWorker';
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 | registerServiceWorker();
10 |
--------------------------------------------------------------------------------
/router-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/router-app/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/vidly-api-node/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | logfile.log
--------------------------------------------------------------------------------
/vidly-api-node/config/custom-environment-variables.json:
--------------------------------------------------------------------------------
1 | {
2 | "jwtPrivateKey": "vidly_jwtPrivateKey"
3 | }
--------------------------------------------------------------------------------
/vidly-api-node/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "jwtPrivateKey": "unsecureKey",
3 | "db": "mongodb://localhost/vidly",
4 | "port": "3900",
5 | "requiresAuth": true
6 | }
7 |
--------------------------------------------------------------------------------
/vidly-api-node/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "jwtPrivateKey": "1234",
3 | "db": "mongodb://localhost/vidly_tests"
4 | }
--------------------------------------------------------------------------------
/vidly-api-node/index.js:
--------------------------------------------------------------------------------
1 | const winston = require("winston");
2 | const express = require("express");
3 | const config = require("config");
4 | const app = express();
5 |
6 | require("./startup/logging")();
7 | require("./startup/cors")(app);
8 | require("./startup/routes")(app);
9 | require("./startup/db")();
10 | require("./startup/config")();
11 | require("./startup/validation")();
12 |
13 | const port = process.env.PORT || config.get("port");
14 | const server = app.listen(port, () =>
15 | winston.info(`Listening on port ${port}...`)
16 | );
17 |
18 | module.exports = server;
19 |
--------------------------------------------------------------------------------
/vidly-api-node/middleware/admin.js:
--------------------------------------------------------------------------------
1 | const config = require("config");
2 |
3 | module.exports = function(req, res, next) {
4 | // 401 Unauthorized
5 | // 403 Forbidden
6 | if (!config.get("requiresAuth")) return next();
7 |
8 | if (!req.user.isAdmin) return res.status(403).send("Access denied.");
9 |
10 | next();
11 | };
12 |
--------------------------------------------------------------------------------
/vidly-api-node/middleware/async.js:
--------------------------------------------------------------------------------
1 | module.exports = function (handler) {
2 | return async (req, res, next) => {
3 | try {
4 | await handler(req, res);
5 | }
6 | catch(ex) {
7 | next(ex);
8 | }
9 | };
10 | }
--------------------------------------------------------------------------------
/vidly-api-node/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const config = require("config");
3 |
4 | module.exports = function(req, res, next) {
5 | if (!config.get("requiresAuth")) return next();
6 |
7 | const token = req.header("x-auth-token");
8 | if (!token) return res.status(401).send("Access denied. No token provided.");
9 |
10 | try {
11 | const decoded = jwt.verify(token, config.get("jwtPrivateKey"));
12 | req.user = decoded;
13 | next();
14 | } catch (ex) {
15 | res.status(400).send("Invalid token.");
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/vidly-api-node/middleware/error.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 |
3 | module.exports = function(err, req, res, next){
4 | winston.error(err.message, err);
5 |
6 | // error
7 | // warn
8 | // info
9 | // verbose
10 | // debug
11 | // silly
12 |
13 | res.status(500).send('Something failed.');
14 | }
--------------------------------------------------------------------------------
/vidly-api-node/middleware/validate.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (validator) => {
3 | return (req, res, next) => {
4 | const { error } = validator(req.body);
5 | if (error) return res.status(400).send(error.details[0].message);
6 | next();
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vidly-api-node/middleware/validateObjectId.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | module.exports = function(req, res, next) {
4 | if (!mongoose.Types.ObjectId.isValid(req.params.id))
5 | return res.status(404).send('Invalid ID.');
6 |
7 | next();
8 | }
--------------------------------------------------------------------------------
/vidly-api-node/models/customer.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const mongoose = require('mongoose');
3 |
4 | const Customer = mongoose.model('Customer', new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | minlength: 5,
9 | maxlength: 50
10 | },
11 | isGold: {
12 | type: Boolean,
13 | default: false
14 | },
15 | phone: {
16 | type: String,
17 | required: true,
18 | minlength: 5,
19 | maxlength: 50
20 | }
21 | }));
22 |
23 | function validateCustomer(customer) {
24 | const schema = {
25 | name: Joi.string().min(5).max(50).required(),
26 | phone: Joi.string().min(5).max(50).required(),
27 | isGold: Joi.boolean()
28 | };
29 |
30 | return Joi.validate(customer, schema);
31 | }
32 |
33 | exports.Customer = Customer;
34 | exports.validate = validateCustomer;
--------------------------------------------------------------------------------
/vidly-api-node/models/genre.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const mongoose = require('mongoose');
3 |
4 | const genreSchema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | minlength: 5,
9 | maxlength: 50
10 | }
11 | });
12 |
13 | const Genre = mongoose.model('Genre', genreSchema);
14 |
15 | function validateGenre(genre) {
16 | const schema = {
17 | name: Joi.string().min(5).max(50).required()
18 | };
19 |
20 | return Joi.validate(genre, schema);
21 | }
22 |
23 | exports.genreSchema = genreSchema;
24 | exports.Genre = Genre;
25 | exports.validate = validateGenre;
--------------------------------------------------------------------------------
/vidly-api-node/models/movie.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const mongoose = require('mongoose');
3 | const {genreSchema} = require('./genre');
4 |
5 | const Movie = mongoose.model('Movies', new mongoose.Schema({
6 | title: {
7 | type: String,
8 | required: true,
9 | trim: true,
10 | minlength: 5,
11 | maxlength: 255
12 | },
13 | genre: {
14 | type: genreSchema,
15 | required: true
16 | },
17 | numberInStock: {
18 | type: Number,
19 | required: true,
20 | min: 0,
21 | max: 255
22 | },
23 | dailyRentalRate: {
24 | type: Number,
25 | required: true,
26 | min: 0,
27 | max: 255
28 | }
29 | }));
30 |
31 | function validateMovie(movie) {
32 | const schema = {
33 | title: Joi.string().min(5).max(50).required(),
34 | genreId: Joi.objectId().required(),
35 | numberInStock: Joi.number().min(0).required(),
36 | dailyRentalRate: Joi.number().min(0).required()
37 | };
38 |
39 | return Joi.validate(movie, schema);
40 | }
41 |
42 | exports.Movie = Movie;
43 | exports.validate = validateMovie;
--------------------------------------------------------------------------------
/vidly-api-node/models/rental.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const mongoose = require('mongoose');
3 | const moment = require('moment');
4 |
5 | const rentalSchema = new mongoose.Schema({
6 | customer: {
7 | type: new mongoose.Schema({
8 | name: {
9 | type: String,
10 | required: true,
11 | minlength: 5,
12 | maxlength: 50
13 | },
14 | isGold: {
15 | type: Boolean,
16 | default: false
17 | },
18 | phone: {
19 | type: String,
20 | required: true,
21 | minlength: 5,
22 | maxlength: 50
23 | }
24 | }),
25 | required: true
26 | },
27 | movie: {
28 | type: new mongoose.Schema({
29 | title: {
30 | type: String,
31 | required: true,
32 | trim: true,
33 | minlength: 5,
34 | maxlength: 255
35 | },
36 | dailyRentalRate: {
37 | type: Number,
38 | required: true,
39 | min: 0,
40 | max: 255
41 | }
42 | }),
43 | required: true
44 | },
45 | dateOut: {
46 | type: Date,
47 | required: true,
48 | default: Date.now
49 | },
50 | dateReturned: {
51 | type: Date
52 | },
53 | rentalFee: {
54 | type: Number,
55 | min: 0
56 | }
57 | });
58 |
59 | rentalSchema.statics.lookup = function(customerId, movieId) {
60 | return this.findOne({
61 | 'customer._id': customerId,
62 | 'movie._id': movieId,
63 | });
64 | }
65 |
66 | rentalSchema.methods.return = function() {
67 | this.dateReturned = new Date();
68 |
69 | const rentalDays = moment().diff(this.dateOut, 'days');
70 | this.rentalFee = rentalDays * this.movie.dailyRentalRate;
71 | }
72 |
73 | const Rental = mongoose.model('Rental', rentalSchema);
74 |
75 | function validateRental(rental) {
76 | const schema = {
77 | customerId: Joi.objectId().required(),
78 | movieId: Joi.objectId().required()
79 | };
80 |
81 | return Joi.validate(rental, schema);
82 | }
83 |
84 | exports.Rental = Rental;
85 | exports.validate = validateRental;
--------------------------------------------------------------------------------
/vidly-api-node/models/user.js:
--------------------------------------------------------------------------------
1 | const config = require("config");
2 | const jwt = require("jsonwebtoken");
3 | const Joi = require("joi");
4 | const mongoose = require("mongoose");
5 |
6 | const userSchema = new mongoose.Schema({
7 | name: {
8 | type: String,
9 | required: true,
10 | minlength: 2,
11 | maxlength: 50
12 | },
13 | email: {
14 | type: String,
15 | required: true,
16 | minlength: 5,
17 | maxlength: 255,
18 | unique: true
19 | },
20 | password: {
21 | type: String,
22 | required: true,
23 | minlength: 5,
24 | maxlength: 1024
25 | },
26 | isAdmin: Boolean
27 | });
28 |
29 | userSchema.methods.generateAuthToken = function() {
30 | const token = jwt.sign(
31 | {
32 | _id: this._id,
33 | name: this.name,
34 | email: this.email,
35 | isAdmin: this.isAdmin
36 | },
37 | config.get("jwtPrivateKey")
38 | );
39 | return token;
40 | };
41 |
42 | const User = mongoose.model("User", userSchema);
43 |
44 | function validateUser(user) {
45 | const schema = {
46 | name: Joi.string()
47 | .min(2)
48 | .max(50)
49 | .required(),
50 | email: Joi.string()
51 | .min(5)
52 | .max(255)
53 | .required()
54 | .email(),
55 | password: Joi.string()
56 | .min(5)
57 | .max(255)
58 | .required()
59 | };
60 |
61 | return Joi.validate(user, schema);
62 | }
63 |
64 | exports.User = User;
65 | exports.validate = validateUser;
66 |
--------------------------------------------------------------------------------
/vidly-api-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vidly",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "seed": "node seed.js",
9 | "test": "jest --watchAll --verbose"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "bcrypt": "^1.0.3",
16 | "config": "^1.29.4",
17 | "cors": "^2.8.4",
18 | "express": "^4.16.2",
19 | "express-async-errors": "^2.1.0",
20 | "fawn": "^2.1.5",
21 | "joi": "^13.4.0",
22 | "joi-objectid": "^2.0.0",
23 | "jsonwebtoken": "^8.1.1",
24 | "lodash": "^4.17.10",
25 | "moment": "^2.20.1",
26 | "mongoose": "^5.0.2",
27 | "winston": "^2.4.0",
28 | "winston-mongodb": "^3.0.0"
29 | },
30 | "devDependencies": {
31 | "jest": "^22.2.2",
32 | "supertest": "^3.0.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/vidly-api-node/readme.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | This project is the backend of Vidly, an imaginary video rental app. I've used Vidly as an example in several of my online programming courses, such as:
4 |
5 | - https://codewithmosh.com/p/mastering-react
6 | - https://codewithmosh.com/p/the-complete-node-js-course
7 | - https://codewithmosh.com/p/asp-net-mvc
8 |
9 | This is the implementation of Vidly in Node.js.
10 |
11 | ## Setup
12 |
13 | Make sure to follow all these steps exactly as explained below. Do not miss any steps or you won't be able to run this application.
14 |
15 | ### Install MongoDB
16 |
17 | To run this project, you need to install the latest version of MongoDB Community Edition first.
18 |
19 | https://docs.mongodb.com/manual/installation/
20 |
21 | Once you install MongoDB, make sure it's running.
22 |
23 | ### Install the Dependencies
24 |
25 | Next, from the project folder, install the dependencies:
26 |
27 | npm i
28 |
29 | ### Populate the Database
30 |
31 | node seed.js
32 |
33 | ### Run the Tests
34 |
35 | You're almost done! Run the tests to make sure everything is working:
36 |
37 | npm test
38 |
39 | All tests should pass.
40 |
41 | ### Start the Server
42 |
43 | node index.js
44 |
45 | This will launch the Node server on port 3900. If that port is busy, you can set a different point in config/default.json.
46 |
47 | Open up your browser and head over to:
48 |
49 | http://localhost:3900/api/genres
50 |
51 | You should see the list of genres. That confirms that you have set up everything successfully.
52 |
53 | ### (Optional) Environment Variables
54 |
55 | If you look at config/default.json, you'll see a property called jwtPrivateKey. This key is used to encrypt JSON web tokens. So, for security reasons, it should not be checked into the source control. I've set a default value here to make it easier for you to get up and running with this project. For a production scenario, you should store this key as an environment variable.
56 |
57 | On Mac:
58 |
59 | export vidly_jwtPrivateKey=yourSecureKey
60 |
61 | On Windows:
62 |
63 | set vidly_jwtPrivateKey=yourSecureKey
64 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/auth.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const bcrypt = require('bcrypt');
3 | const _ = require('lodash');
4 | const {User} = require('../models/user');
5 | const mongoose = require('mongoose');
6 | const express = require('express');
7 | const router = express.Router();
8 |
9 | router.post('/', async (req, res) => {
10 | const { error } = validate(req.body);
11 | if (error) return res.status(400).send(error.details[0].message);
12 |
13 | let user = await User.findOne({ email: req.body.email });
14 | if (!user) return res.status(400).send('Invalid email or password.');
15 |
16 | const validPassword = await bcrypt.compare(req.body.password, user.password);
17 | if (!validPassword) return res.status(400).send('Invalid email or password.');
18 |
19 | const token = user.generateAuthToken();
20 | res.send(token);
21 | });
22 |
23 | function validate(req) {
24 | const schema = {
25 | email: Joi.string().min(5).max(255).required().email(),
26 | password: Joi.string().min(5).max(255).required()
27 | };
28 |
29 | return Joi.validate(req, schema);
30 | }
31 |
32 | module.exports = router;
33 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/customers.js:
--------------------------------------------------------------------------------
1 | const { Customer, validate } = require("../models/customer");
2 | const auth = require("../middleware/auth");
3 | const express = require("express");
4 | const router = express.Router();
5 |
6 | router.get("/", auth, async (req, res) => {
7 | const customers = await Customer.find()
8 | .select("-__v")
9 | .sort("name");
10 | res.send(customers);
11 | });
12 |
13 | router.post("/", auth, async (req, res) => {
14 | const { error } = validate(req.body);
15 | if (error) return res.status(400).send(error.details[0].message);
16 |
17 | let customer = new Customer({
18 | name: req.body.name,
19 | isGold: req.body.isGold,
20 | phone: req.body.phone
21 | });
22 | customer = await customer.save();
23 |
24 | res.send(customer);
25 | });
26 |
27 | router.put("/:id", auth, async (req, res) => {
28 | const { error } = validate(req.body);
29 | if (error) return res.status(400).send(error.details[0].message);
30 |
31 | const customer = await Customer.findByIdAndUpdate(
32 | req.params.id,
33 | {
34 | name: req.body.name,
35 | isGold: req.body.isGold,
36 | phone: req.body.phone
37 | },
38 | { new: true }
39 | );
40 |
41 | if (!customer)
42 | return res
43 | .status(404)
44 | .send("The customer with the given ID was not found.");
45 |
46 | res.send(customer);
47 | });
48 |
49 | router.delete("/:id", auth, async (req, res) => {
50 | const customer = await Customer.findByIdAndRemove(req.params.id);
51 |
52 | if (!customer)
53 | return res
54 | .status(404)
55 | .send("The customer with the given ID was not found.");
56 |
57 | res.send(customer);
58 | });
59 |
60 | router.get("/:id", auth, async (req, res) => {
61 | const customer = await Customer.findById(req.params.id).select("-__v");
62 |
63 | if (!customer)
64 | return res
65 | .status(404)
66 | .send("The customer with the given ID was not found.");
67 |
68 | res.send(customer);
69 | });
70 |
71 | module.exports = router;
72 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/genres.js:
--------------------------------------------------------------------------------
1 | const validateObjectId = require("../middleware/validateObjectId");
2 | const auth = require("../middleware/auth");
3 | const admin = require("../middleware/admin");
4 | const { Genre, validate } = require("../models/genre");
5 | const mongoose = require("mongoose");
6 | const express = require("express");
7 | const router = express.Router();
8 |
9 | router.get("/", async (req, res) => {
10 | const genres = await Genre.find()
11 | .select("-__v")
12 | .sort("name");
13 | res.send(genres);
14 | });
15 |
16 | router.post("/", auth, async (req, res) => {
17 | const { error } = validate(req.body);
18 | if (error) return res.status(400).send(error.details[0].message);
19 |
20 | let genre = new Genre({ name: req.body.name });
21 | genre = await genre.save();
22 |
23 | res.send(genre);
24 | });
25 |
26 | router.put("/:id", [auth, validateObjectId], async (req, res) => {
27 | const { error } = validate(req.body);
28 | if (error) return res.status(400).send(error.details[0].message);
29 |
30 | const genre = await Genre.findByIdAndUpdate(
31 | req.params.id,
32 | { name: req.body.name },
33 | {
34 | new: true
35 | }
36 | );
37 |
38 | if (!genre)
39 | return res.status(404).send("The genre with the given ID was not found.");
40 |
41 | res.send(genre);
42 | });
43 |
44 | router.delete("/:id", [auth, admin, validateObjectId], async (req, res) => {
45 | const genre = await Genre.findByIdAndRemove(req.params.id);
46 |
47 | if (!genre)
48 | return res.status(404).send("The genre with the given ID was not found.");
49 |
50 | res.send(genre);
51 | });
52 |
53 | router.get("/:id", validateObjectId, async (req, res) => {
54 | const genre = await Genre.findById(req.params.id).select("-__v");
55 |
56 | if (!genre)
57 | return res.status(404).send("The genre with the given ID was not found.");
58 |
59 | res.send(genre);
60 | });
61 |
62 | module.exports = router;
63 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/movies.js:
--------------------------------------------------------------------------------
1 | const { Movie, validate } = require("../models/movie");
2 | const { Genre } = require("../models/genre");
3 | const auth = require("../middleware/auth");
4 | const admin = require("../middleware/admin");
5 | const validateObjectId = require("../middleware/validateObjectId");
6 | const moment = require("moment");
7 | const mongoose = require("mongoose");
8 | const express = require("express");
9 | const router = express.Router();
10 |
11 | router.get("/", async (req, res) => {
12 | const movies = await Movie.find()
13 | .select("-__v")
14 | .sort("name");
15 | res.send(movies);
16 | });
17 |
18 | router.post("/", [auth], async (req, res) => {
19 | const { error } = validate(req.body);
20 | if (error) return res.status(400).send(error.details[0].message);
21 |
22 | const genre = await Genre.findById(req.body.genreId);
23 | if (!genre) return res.status(400).send("Invalid genre.");
24 |
25 | const movie = new Movie({
26 | title: req.body.title,
27 | genre: {
28 | _id: genre._id,
29 | name: genre.name
30 | },
31 | numberInStock: req.body.numberInStock,
32 | dailyRentalRate: req.body.dailyRentalRate,
33 | publishDate: moment().toJSON()
34 | });
35 | await movie.save();
36 |
37 | res.send(movie);
38 | });
39 |
40 | router.put("/:id", [auth], async (req, res) => {
41 | const { error } = validate(req.body);
42 | if (error) return res.status(400).send(error.details[0].message);
43 |
44 | const genre = await Genre.findById(req.body.genreId);
45 | if (!genre) return res.status(400).send("Invalid genre.");
46 |
47 | const movie = await Movie.findByIdAndUpdate(
48 | req.params.id,
49 | {
50 | title: req.body.title,
51 | genre: {
52 | _id: genre._id,
53 | name: genre.name
54 | },
55 | numberInStock: req.body.numberInStock,
56 | dailyRentalRate: req.body.dailyRentalRate
57 | },
58 | { new: true }
59 | );
60 |
61 | if (!movie)
62 | return res.status(404).send("The movie with the given ID was not found.");
63 |
64 | res.send(movie);
65 | });
66 |
67 | router.delete("/:id", [auth, admin], async (req, res) => {
68 | const movie = await Movie.findByIdAndRemove(req.params.id);
69 |
70 | if (!movie)
71 | return res.status(404).send("The movie with the given ID was not found.");
72 |
73 | res.send(movie);
74 | });
75 |
76 | router.get("/:id", validateObjectId, async (req, res) => {
77 | const movie = await Movie.findById(req.params.id).select("-__v");
78 |
79 | if (!movie)
80 | return res.status(404).send("The movie with the given ID was not found.");
81 |
82 | res.send(movie);
83 | });
84 |
85 | module.exports = router;
86 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/rentals.js:
--------------------------------------------------------------------------------
1 | const { Rental, validate } = require("../models/rental");
2 | const { Movie } = require("../models/movie");
3 | const { Customer } = require("../models/customer");
4 | const auth = require("../middleware/auth");
5 | const mongoose = require("mongoose");
6 | const Fawn = require("fawn");
7 | const express = require("express");
8 | const router = express.Router();
9 |
10 | Fawn.init(mongoose);
11 |
12 | router.get("/", auth, async (req, res) => {
13 | const rentals = await Rental.find()
14 | .select("-__v")
15 | .sort("-dateOut");
16 | res.send(rentals);
17 | });
18 |
19 | router.post("/", auth, async (req, res) => {
20 | const { error } = validate(req.body);
21 | if (error) return res.status(400).send(error.details[0].message);
22 |
23 | const customer = await Customer.findById(req.body.customerId);
24 | if (!customer) return res.status(400).send("Invalid customer.");
25 |
26 | const movie = await Movie.findById(req.body.movieId);
27 | if (!movie) return res.status(400).send("Invalid movie.");
28 |
29 | if (movie.numberInStock === 0)
30 | return res.status(400).send("Movie not in stock.");
31 |
32 | let rental = new Rental({
33 | customer: {
34 | _id: customer._id,
35 | name: customer.name,
36 | phone: customer.phone
37 | },
38 | movie: {
39 | _id: movie._id,
40 | title: movie.title,
41 | dailyRentalRate: movie.dailyRentalRate
42 | }
43 | });
44 |
45 | try {
46 | new Fawn.Task()
47 | .save("rentals", rental)
48 | .update(
49 | "movies",
50 | { _id: movie._id },
51 | {
52 | $inc: { numberInStock: -1 }
53 | }
54 | )
55 | .run();
56 |
57 | res.send(rental);
58 | } catch (ex) {
59 | res.status(500).send("Something failed.");
60 | }
61 | });
62 |
63 | router.get("/:id", [auth], async (req, res) => {
64 | const rental = await Rental.findById(req.params.id).select("-__v");
65 |
66 | if (!rental)
67 | return res.status(404).send("The rental with the given ID was not found.");
68 |
69 | res.send(rental);
70 | });
71 |
72 | module.exports = router;
73 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/returns.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const validate = require("../middleware/validate");
3 | const { Rental } = require("../models/rental");
4 | const { Movie } = require("../models/movie");
5 | const auth = require("../middleware/auth");
6 | const express = require("express");
7 | const router = express.Router();
8 |
9 | router.post("/", [auth, validate(validateReturn)], async (req, res) => {
10 | const rental = await Rental.lookup(req.body.customerId, req.body.movieId);
11 |
12 | if (!rental) return res.status(404).send("Rental not found.");
13 |
14 | if (rental.dateReturned)
15 | return res.status(400).send("Return already processed.");
16 |
17 | rental.return();
18 | await rental.save();
19 |
20 | await Movie.update(
21 | { _id: rental.movie._id },
22 | {
23 | $inc: { numberInStock: 1 }
24 | }
25 | );
26 |
27 | return res.send(rental);
28 | });
29 |
30 | function validateReturn(req) {
31 | const schema = {
32 | customerId: Joi.objectId().required(),
33 | movieId: Joi.objectId().required()
34 | };
35 |
36 | return Joi.validate(req, schema);
37 | }
38 |
39 | module.exports = router;
40 |
--------------------------------------------------------------------------------
/vidly-api-node/routes/users.js:
--------------------------------------------------------------------------------
1 | const auth = require("../middleware/auth");
2 | const bcrypt = require("bcrypt");
3 | const _ = require("lodash");
4 | const { User, validate } = require("../models/user");
5 | const express = require("express");
6 | const router = express.Router();
7 |
8 | router.get("/me", auth, async (req, res) => {
9 | const user = await User.findById(req.user._id).select("-password");
10 | res.send(user);
11 | });
12 |
13 | router.post("/", async (req, res) => {
14 | const { error } = validate(req.body);
15 | if (error) return res.status(400).send(error.details[0].message);
16 |
17 | let user = await User.findOne({ email: req.body.email });
18 | if (user) return res.status(400).send("User already registered.");
19 |
20 | user = new User(_.pick(req.body, ["name", "email", "password"]));
21 | const salt = await bcrypt.genSalt(10);
22 | user.password = await bcrypt.hash(user.password, salt);
23 | await user.save();
24 |
25 | const token = user.generateAuthToken();
26 | res
27 | .header("x-auth-token", token)
28 | .header("access-control-expose-headers", "x-auth-token")
29 | .send(_.pick(user, ["_id", "name", "email"]));
30 | });
31 |
32 | module.exports = router;
33 |
--------------------------------------------------------------------------------
/vidly-api-node/seed.js:
--------------------------------------------------------------------------------
1 | const { Genre } = require("./models/genre");
2 | const { Movie } = require("./models/movie");
3 | const mongoose = require("mongoose");
4 | const config = require("config");
5 |
6 | const data = [
7 | {
8 | name: "Comedy",
9 | movies: [
10 | { title: "Airplane", numberInStock: 5, dailyRentalRate: 2 },
11 | { title: "The Hangover", numberInStock: 10, dailyRentalRate: 2 },
12 | { title: "Wedding Crashers", numberInStock: 15, dailyRentalRate: 2 }
13 | ]
14 | },
15 | {
16 | name: "Action",
17 | movies: [
18 | { title: "Die Hard", numberInStock: 5, dailyRentalRate: 2 },
19 | { title: "Terminator", numberInStock: 10, dailyRentalRate: 2 },
20 | { title: "The Avengers", numberInStock: 15, dailyRentalRate: 2 }
21 | ]
22 | },
23 | {
24 | name: "Romance",
25 | movies: [
26 | { title: "The Notebook", numberInStock: 5, dailyRentalRate: 2 },
27 | { title: "When Harry Met Sally", numberInStock: 10, dailyRentalRate: 2 },
28 | { title: "Pretty Woman", numberInStock: 15, dailyRentalRate: 2 }
29 | ]
30 | },
31 | {
32 | name: "Thriller",
33 | movies: [
34 | { title: "The Sixth Sense", numberInStock: 5, dailyRentalRate: 2 },
35 | { title: "Gone Girl", numberInStock: 10, dailyRentalRate: 2 },
36 | { title: "The Others", numberInStock: 15, dailyRentalRate: 2 }
37 | ]
38 | }
39 | ];
40 |
41 | async function seed() {
42 | await mongoose.connect(config.get("db"));
43 |
44 | await Movie.deleteMany({});
45 | await Genre.deleteMany({});
46 |
47 | for (let genre of data) {
48 | const { _id: genreId } = await new Genre({ name: genre.name }).save();
49 | const movies = genre.movies.map(movie => ({
50 | ...movie,
51 | genre: { _id: genreId, name: genre.name }
52 | }));
53 | await Movie.insertMany(movies);
54 | }
55 |
56 | mongoose.disconnect();
57 |
58 | console.info("Done!");
59 | }
60 |
61 | seed();
62 |
--------------------------------------------------------------------------------
/vidly-api-node/startup/config.js:
--------------------------------------------------------------------------------
1 | const config = require('config');
2 |
3 | module.exports = function() {
4 | if (!config.get('jwtPrivateKey')) {
5 | throw new Error('FATAL ERROR: jwtPrivateKey is not defined.');
6 | }
7 | }
--------------------------------------------------------------------------------
/vidly-api-node/startup/cors.js:
--------------------------------------------------------------------------------
1 | const cors = require("cors");
2 |
3 | module.exports = function(app) {
4 | app.use(cors());
5 | };
6 |
--------------------------------------------------------------------------------
/vidly-api-node/startup/db.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | const mongoose = require('mongoose');
3 | const config = require('config');
4 |
5 | module.exports = function() {
6 | const db = config.get('db');
7 | mongoose.connect(db)
8 | .then(() => winston.info(`Connected to ${db}...`));
9 | }
--------------------------------------------------------------------------------
/vidly-api-node/startup/logging.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | // require('winston-mongodb');
3 | require('express-async-errors');
4 |
5 | module.exports = function() {
6 | winston.handleExceptions(
7 | new winston.transports.Console({ colorize: true, prettyPrint: true }),
8 | new winston.transports.File({ filename: 'uncaughtExceptions.log' }));
9 |
10 | process.on('unhandledRejection', (ex) => {
11 | throw ex;
12 | });
13 |
14 | winston.add(winston.transports.File, { filename: 'logfile.log' });
15 | // winston.add(winston.transports.MongoDB, {
16 | // db: 'mongodb://localhost/vidly',
17 | // level: 'info'
18 | // });
19 | }
--------------------------------------------------------------------------------
/vidly-api-node/startup/routes.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const genres = require('../routes/genres');
3 | const customers = require('../routes/customers');
4 | const movies = require('../routes/movies');
5 | const rentals = require('../routes/rentals');
6 | const users = require('../routes/users');
7 | const auth = require('../routes/auth');
8 | const returns = require('../routes/returns');
9 | const error = require('../middleware/error');
10 |
11 | module.exports = function(app) {
12 | app.use(express.json());
13 | app.use('/api/genres', genres);
14 | app.use('/api/customers', customers);
15 | app.use('/api/movies', movies);
16 | app.use('/api/rentals', rentals);
17 | app.use('/api/users', users);
18 | app.use('/api/auth', auth);
19 | app.use('/api/returns', returns);
20 | app.use(error);
21 | }
--------------------------------------------------------------------------------
/vidly-api-node/startup/validation.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 |
3 | module.exports = function() {
4 | Joi.objectId = require('joi-objectid')(Joi);
5 | }
--------------------------------------------------------------------------------
/vidly-api-node/tests/integration/auth.test.js:
--------------------------------------------------------------------------------
1 | const {User} = require('../../models/user');
2 | const {Genre} = require('../../models/genre');
3 | const request = require('supertest');
4 |
5 | describe('auth middleware', () => {
6 | beforeEach(() => { server = require('../../index'); })
7 | afterEach(async () => {
8 | await Genre.remove({});
9 | await server.close();
10 | });
11 |
12 | let token;
13 |
14 | const exec = () => {
15 | return request(server)
16 | .post('/api/genres')
17 | .set('x-auth-token', token)
18 | .send({ name: 'genre1' });
19 | }
20 |
21 | beforeEach(() => {
22 | token = new User().generateAuthToken();
23 | });
24 |
25 | it('should return 401 if no token is provided', async () => {
26 | token = '';
27 |
28 | const res = await exec();
29 |
30 | expect(res.status).toBe(401);
31 | });
32 |
33 | it('should return 400 if token is invalid', async () => {
34 | token = 'a';
35 |
36 | const res = await exec();
37 |
38 | expect(res.status).toBe(400);
39 | });
40 |
41 | it('should return 200 if token is valid', async () => {
42 | const res = await exec();
43 |
44 | expect(res.status).toBe(200);
45 | });
46 | });
--------------------------------------------------------------------------------
/vidly-api-node/tests/integration/genres.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const {Genre} = require('../../models/genre');
3 | const {User} = require('../../models/user');
4 | const mongoose = require('mongoose');
5 |
6 | let server;
7 |
8 | describe('/api/genres', () => {
9 | beforeEach(() => { server = require('../../index'); })
10 | afterEach(async () => {
11 | await server.close();
12 | await Genre.remove({});
13 | });
14 |
15 | describe('GET /', () => {
16 | it('should return all genres', async () => {
17 | const genres = [
18 | { name: 'genre1' },
19 | { name: 'genre2' },
20 | ];
21 |
22 | await Genre.collection.insertMany(genres);
23 |
24 | const res = await request(server).get('/api/genres');
25 |
26 | expect(res.status).toBe(200);
27 | expect(res.body.length).toBe(2);
28 | expect(res.body.some(g => g.name === 'genre1')).toBeTruthy();
29 | expect(res.body.some(g => g.name === 'genre2')).toBeTruthy();
30 | });
31 | });
32 |
33 | describe('GET /:id', () => {
34 | it('should return a genre if valid id is passed', async () => {
35 | const genre = new Genre({ name: 'genre1' });
36 | await genre.save();
37 |
38 | const res = await request(server).get('/api/genres/' + genre._id);
39 |
40 | expect(res.status).toBe(200);
41 | expect(res.body).toHaveProperty('name', genre.name);
42 | });
43 |
44 | it('should return 404 if invalid id is passed', async () => {
45 | const res = await request(server).get('/api/genres/1');
46 |
47 | expect(res.status).toBe(404);
48 | });
49 |
50 | it('should return 404 if no genre with the given id exists', async () => {
51 | const id = mongoose.Types.ObjectId();
52 | const res = await request(server).get('/api/genres/' + id);
53 |
54 | expect(res.status).toBe(404);
55 | });
56 | });
57 |
58 | describe('POST /', () => {
59 |
60 | // Define the happy path, and then in each test, we change
61 | // one parameter that clearly aligns with the name of the
62 | // test.
63 | let token;
64 | let name;
65 |
66 | const exec = async () => {
67 | return await request(server)
68 | .post('/api/genres')
69 | .set('x-auth-token', token)
70 | .send({ name });
71 | }
72 |
73 | beforeEach(() => {
74 | token = new User().generateAuthToken();
75 | name = 'genre1';
76 | })
77 |
78 | it('should return 401 if client is not logged in', async () => {
79 | token = '';
80 |
81 | const res = await exec();
82 |
83 | expect(res.status).toBe(401);
84 | });
85 |
86 | it('should return 400 if genre is less than 5 characters', async () => {
87 | name = '1234';
88 |
89 | const res = await exec();
90 |
91 | expect(res.status).toBe(400);
92 | });
93 |
94 | it('should return 400 if genre is more than 50 characters', async () => {
95 | name = new Array(52).join('a');
96 |
97 | const res = await exec();
98 |
99 | expect(res.status).toBe(400);
100 | });
101 |
102 | it('should save the genre if it is valid', async () => {
103 | await exec();
104 |
105 | const genre = await Genre.find({ name: 'genre1' });
106 |
107 | expect(genre).not.toBeNull();
108 | });
109 |
110 | it('should return the genre if it is valid', async () => {
111 | const res = await exec();
112 |
113 | expect(res.body).toHaveProperty('_id');
114 | expect(res.body).toHaveProperty('name', 'genre1');
115 | });
116 | });
117 |
118 | describe('PUT /:id', () => {
119 | let token;
120 | let newName;
121 | let genre;
122 | let id;
123 |
124 | const exec = async () => {
125 | return await request(server)
126 | .put('/api/genres/' + id)
127 | .set('x-auth-token', token)
128 | .send({ name: newName });
129 | }
130 |
131 | beforeEach(async () => {
132 | // Before each test we need to create a genre and
133 | // put it in the database.
134 | genre = new Genre({ name: 'genre1' });
135 | await genre.save();
136 |
137 | token = new User().generateAuthToken();
138 | id = genre._id;
139 | newName = 'updatedName';
140 | })
141 |
142 | it('should return 401 if client is not logged in', async () => {
143 | token = '';
144 |
145 | const res = await exec();
146 |
147 | expect(res.status).toBe(401);
148 | });
149 |
150 | it('should return 400 if genre is less than 5 characters', async () => {
151 | newName = '1234';
152 |
153 | const res = await exec();
154 |
155 | expect(res.status).toBe(400);
156 | });
157 |
158 | it('should return 400 if genre is more than 50 characters', async () => {
159 | newName = new Array(52).join('a');
160 |
161 | const res = await exec();
162 |
163 | expect(res.status).toBe(400);
164 | });
165 |
166 | it('should return 404 if id is invalid', async () => {
167 | id = 1;
168 |
169 | const res = await exec();
170 |
171 | expect(res.status).toBe(404);
172 | });
173 |
174 | it('should return 404 if genre with the given id was not found', async () => {
175 | id = mongoose.Types.ObjectId();
176 |
177 | const res = await exec();
178 |
179 | expect(res.status).toBe(404);
180 | });
181 |
182 | it('should update the genre if input is valid', async () => {
183 | await exec();
184 |
185 | const updatedGenre = await Genre.findById(genre._id);
186 |
187 | expect(updatedGenre.name).toBe(newName);
188 | });
189 |
190 | it('should return the updated genre if it is valid', async () => {
191 | const res = await exec();
192 |
193 | expect(res.body).toHaveProperty('_id');
194 | expect(res.body).toHaveProperty('name', newName);
195 | });
196 | });
197 |
198 | describe('DELETE /:id', () => {
199 | let token;
200 | let genre;
201 | let id;
202 |
203 | const exec = async () => {
204 | return await request(server)
205 | .delete('/api/genres/' + id)
206 | .set('x-auth-token', token)
207 | .send();
208 | }
209 |
210 | beforeEach(async () => {
211 | // Before each test we need to create a genre and
212 | // put it in the database.
213 | genre = new Genre({ name: 'genre1' });
214 | await genre.save();
215 |
216 | id = genre._id;
217 | token = new User({ isAdmin: true }).generateAuthToken();
218 | })
219 |
220 | it('should return 401 if client is not logged in', async () => {
221 | token = '';
222 |
223 | const res = await exec();
224 |
225 | expect(res.status).toBe(401);
226 | });
227 |
228 | it('should return 403 if the user is not an admin', async () => {
229 | token = new User({ isAdmin: false }).generateAuthToken();
230 |
231 | const res = await exec();
232 |
233 | expect(res.status).toBe(403);
234 | });
235 |
236 | it('should return 404 if id is invalid', async () => {
237 | id = 1;
238 |
239 | const res = await exec();
240 |
241 | expect(res.status).toBe(404);
242 | });
243 |
244 | it('should return 404 if no genre with the given id was found', async () => {
245 | id = mongoose.Types.ObjectId();
246 |
247 | const res = await exec();
248 |
249 | expect(res.status).toBe(404);
250 | });
251 |
252 | it('should delete the genre if input is valid', async () => {
253 | await exec();
254 |
255 | const genreInDb = await Genre.findById(id);
256 |
257 | expect(genreInDb).toBeNull();
258 | });
259 |
260 | it('should return the removed genre', async () => {
261 | const res = await exec();
262 |
263 | expect(res.body).toHaveProperty('_id', genre._id.toHexString());
264 | expect(res.body).toHaveProperty('name', genre.name);
265 | });
266 | });
267 | });
--------------------------------------------------------------------------------
/vidly-api-node/tests/integration/returns.test.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const request = require('supertest');
3 | const {Rental} = require('../../models/rental');
4 | const {Movie} = require('../../models/movie');
5 | const {User} = require('../../models/user');
6 | const mongoose = require('mongoose');
7 |
8 | describe('/api/returns', () => {
9 | let server;
10 | let customerId;
11 | let movieId;
12 | let rental;
13 | let movie;
14 | let token;
15 |
16 | const exec = () => {
17 | return request(server)
18 | .post('/api/returns')
19 | .set('x-auth-token', token)
20 | .send({ customerId, movieId });
21 | };
22 |
23 | beforeEach(async () => {
24 | server = require('../../index');
25 |
26 | customerId = mongoose.Types.ObjectId();
27 | movieId = mongoose.Types.ObjectId();
28 | token = new User().generateAuthToken();
29 |
30 | movie = new Movie({
31 | _id: movieId,
32 | title: '12345',
33 | dailyRentalRate: 2,
34 | genre: { name: '12345' },
35 | numberInStock: 10
36 | });
37 | await movie.save();
38 |
39 | rental = new Rental({
40 | customer: {
41 | _id: customerId,
42 | name: '12345',
43 | phone: '12345'
44 | },
45 | movie: {
46 | _id: movieId,
47 | title: '12345',
48 | dailyRentalRate: 2
49 | }
50 | });
51 | await rental.save();
52 | });
53 |
54 | afterEach(async () => {
55 | await server.close();
56 | await Rental.remove({});
57 | await Movie.remove({});
58 | });
59 |
60 | it('should return 401 if client is not logged in', async () => {
61 | token = '';
62 |
63 | const res = await exec();
64 |
65 | expect(res.status).toBe(401);
66 | });
67 |
68 | it('should return 400 if customerId is not provided', async () => {
69 | customerId = '';
70 |
71 | const res = await exec();
72 |
73 | expect(res.status).toBe(400);
74 | });
75 |
76 | it('should return 400 if movieId is not provided', async () => {
77 | movieId = '';
78 |
79 | const res = await exec();
80 |
81 | expect(res.status).toBe(400);
82 | });
83 |
84 | it('should return 404 if no rental found for the customer/movie', async () => {
85 | await Rental.remove({});
86 |
87 | const res = await exec();
88 |
89 | expect(res.status).toBe(404);
90 | });
91 |
92 | it('should return 400 if return is already processed', async () => {
93 | rental.dateReturned = new Date();
94 | await rental.save();
95 |
96 | const res = await exec();
97 |
98 | expect(res.status).toBe(400);
99 | });
100 |
101 | it('should return 200 if we have a valid request', async () => {
102 | const res = await exec();
103 |
104 | expect(res.status).toBe(200);
105 | });
106 |
107 | it('should set the returnDate if input is valid', async () => {
108 | const res = await exec();
109 |
110 | const rentalInDb = await Rental.findById(rental._id);
111 | const diff = new Date() - rentalInDb.dateReturned;
112 | expect(diff).toBeLessThan(10 * 1000);
113 | });
114 |
115 | it('should set the rentalFee if input is valid', async () => {
116 | rental.dateOut = moment().add(-7, 'days').toDate();
117 | await rental.save();
118 |
119 | const res = await exec();
120 |
121 | const rentalInDb = await Rental.findById(rental._id);
122 | expect(rentalInDb.rentalFee).toBe(14);
123 | });
124 |
125 | it('should increase the movie stock if input is valid', async () => {
126 | const res = await exec();
127 |
128 | const movieInDb = await Movie.findById(movieId);
129 | expect(movieInDb.numberInStock).toBe(movie.numberInStock + 1);
130 | });
131 |
132 | it('should return the rental if input is valid', async () => {
133 | const res = await exec();
134 |
135 | const rentalInDb = await Rental.findById(rental._id);
136 |
137 | expect(Object.keys(res.body)).toEqual(
138 | expect.arrayContaining(['dateOut', 'dateReturned', 'rentalFee',
139 | 'customer', 'movie']));
140 | });
141 | });
--------------------------------------------------------------------------------
/vidly-api-node/tests/unit/middleware/auth.test.js:
--------------------------------------------------------------------------------
1 | const {User} = require('../../../models/user');
2 | const auth = require('../../../middleware/auth');
3 | const mongoose = require('mongoose');
4 |
5 | describe('auth middleware', () => {
6 | it('should populate req.user with the payload of a valid JWT', () => {
7 | const user = {
8 | _id: mongoose.Types.ObjectId().toHexString(),
9 | isAdmin: true
10 | };
11 | const token = new User(user).generateAuthToken();
12 | const req = {
13 | header: jest.fn().mockReturnValue(token)
14 | };
15 | const res = {};
16 | const next = jest.fn();
17 |
18 | auth(req, res, next);
19 |
20 | expect(req.user).toMatchObject(user);
21 | });
22 | });
--------------------------------------------------------------------------------
/vidly-api-node/tests/unit/models/user.test.js:
--------------------------------------------------------------------------------
1 | const {User} = require('../../../models/user');
2 | const jwt = require('jsonwebtoken');
3 | const config = require('config');
4 | const mongoose = require('mongoose');
5 |
6 | describe('user.generateAuthToken', () => {
7 | it('should return a valid JWT', () => {
8 | const payload = {
9 | _id: new mongoose.Types.ObjectId().toHexString(),
10 | isAdmin: true
11 | };
12 | const user = new User(payload);
13 | const token = user.generateAuthToken();
14 | const decoded = jwt.verify(token, config.get('jwtPrivateKey'));
15 | expect(decoded).toMatchObject(payload);
16 | });
17 | });
--------------------------------------------------------------------------------
/vidly-api-node/uncaughtExceptions.log:
--------------------------------------------------------------------------------
1 | {"date":"Fri Jul 20 2018 10:18:13 GMT-0700 (PDT)","process":{"pid":68764,"uid":501,"gid":20,"cwd":"/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node","execPath":"/usr/local/bin/node","version":"v8.9.1","argv":["/usr/local/bin/node","/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node/index.js"],"memoryUsage":{"rss":59969536,"heapTotal":57044992,"heapUsed":20611200,"external":17867169}},"os":{"loadavg":[2.79296875,2.74169921875,2.95166015625],"uptime":575964},"trace":[{"column":11,"file":"/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node/startup/config.js","function":"module.exports","line":5,"method":"exports","native":false},{"column":28,"file":"/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node/index.js","function":null,"line":10,"method":null,"native":false},{"column":30,"file":"module.js","function":"Module._compile","line":635,"method":"_compile","native":false},{"column":10,"file":"module.js","function":"Module._extensions..js","line":646,"method":".js","native":false},{"column":32,"file":"module.js","function":"Module.load","line":554,"method":"load","native":false},{"column":12,"file":"module.js","function":"tryModuleLoad","line":497,"method":null,"native":false},{"column":3,"file":"module.js","function":"Module._load","line":489,"method":"_load","native":false},{"column":10,"file":"module.js","function":"Module.runMain","line":676,"method":"runMain","native":false},{"column":16,"file":"bootstrap_node.js","function":"startup","line":187,"method":null,"native":false},{"column":3,"file":"bootstrap_node.js","function":null,"line":608,"method":null,"native":false}],"stack":["Error: FATAL ERROR: jwtPrivateKey is not defined."," at module.exports (/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node/startup/config.js:5:11)"," at Object. (/Users/moshfeghhamedani/Desktop/Trash/vidly-api-node/index.js:10:28)"," at Module._compile (module.js:635:30)"," at Object.Module._extensions..js (module.js:646:10)"," at Module.load (module.js:554:32)"," at tryModuleLoad (module.js:497:12)"," at Function.Module._load (module.js:489:3)"," at Function.Module.runMain (module.js:676:10)"," at startup (bootstrap_node.js:187:16)"," at bootstrap_node.js:608:3"],"level":"error","message":"uncaughtException: FATAL ERROR: jwtPrivateKey is not defined.","timestamp":"2018-07-20T17:18:13.911Z"}
2 |
--------------------------------------------------------------------------------
/vidly/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/vidly/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vidly",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "bootstrap": "4.1.1",
8 | "font-awesome": "4.7.0",
9 | "joi-browser": "13.4",
10 | "jwt-decode": "^2.2.0",
11 | "lodash": "^4.17.11",
12 | "react": "^16.4.2",
13 | "react-dom": "^16.4.2",
14 | "react-router-dom": "4.3.1",
15 | "react-scripts": "1.1.5",
16 | "react-toastify": "^5.0.0-rc.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/vidly/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fishstick22/mastering-react-mosh/54b01a22f951e8930c9a9c7bcc8db55a01d809db/vidly/public/favicon.ico
--------------------------------------------------------------------------------
/vidly/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/vidly/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/vidly/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/vidly/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, Redirect, Switch } from "react-router-dom";
3 | import { ToastContainer } from "react-toastify";
4 | import NavBar from "./components/navbar";
5 | import Movies from "./components/movies";
6 | import MovieForm from './components/movieForm';
7 | import Customers from "./components/customers";
8 | import Rentals from "./components/rentals";
9 | import NotFound from "./components/notFound";
10 | import LoginForm from './components/loginForm';
11 | import RegisterForm from "./components/registerForm";
12 | import Logout from "./components/logout";
13 | import ProtectedRoute from "./components/common/protectedRoute";
14 | import auth from "./services/authService";
15 | import "react-toastify/dist/ReactToastify.css";
16 | import "./App.css";
17 |
18 | class App extends Component {
19 | state = {};
20 |
21 | componentDidMount() {
22 | const user = auth.getCurrentUser();
23 | this.setState({ user });
24 | }
25 |
26 | render() {
27 | const { user } = this.state;
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | }
42 | />
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | export default App;
56 |
--------------------------------------------------------------------------------
/vidly/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/vidly/src/components/common/form.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Joi from "joi-browser";
3 | import Input from "./input";
4 | import Select from "./select";
5 |
6 | class Form extends Component {
7 | state = {
8 | data: {},
9 | errors: {}
10 | };
11 |
12 | validate = () => {
13 | const options = { abortEarly: false };
14 | const { error } = Joi.validate(this.state.data, this.schema, options);
15 | if (!error) return null;
16 |
17 | const errors = {};
18 | for (let item of error.details) errors[item.path[0]] = item.message;
19 | return errors;
20 | };
21 |
22 | validateProperty = ({ name, value }) => {
23 | const obj = { [name]: value };
24 | const schema = { [name]: this.schema[name] };
25 | const { error } = Joi.validate(obj, schema);
26 | return error ? error.details[0].message : null;
27 | };
28 |
29 | handleSubmit = e => {
30 | e.preventDefault();
31 |
32 | const errors = this.validate();
33 | this.setState({ errors: errors || {} });
34 | if (errors) return;
35 |
36 | this.doSubmit();
37 | };
38 |
39 | handleChange = ({ currentTarget: input }) => {
40 | const errors = { ...this.state.errors };
41 | const errorMessage = this.validateProperty(input);
42 | if (errorMessage) errors[input.name] = errorMessage;
43 | else delete errors[input.name];
44 |
45 | const data = { ...this.state.data };
46 | data[input.name] = input.value;
47 |
48 | this.setState({ data, errors });
49 | };
50 |
51 | renderButton(label) {
52 | return (
53 |
56 | );
57 | }
58 |
59 | renderSelect(name, label, options) {
60 | const { data, errors } = this.state;
61 |
62 | return (
63 |
71 | );
72 | }
73 |
74 | renderInput(name, label, type = "text") {
75 | const { data, errors } = this.state;
76 |
77 | return (
78 |
86 | );
87 | }
88 | }
89 |
90 | export default Form;
91 |
--------------------------------------------------------------------------------
/vidly/src/components/common/input.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Input = ({ name, label, error, ...rest }) => {
4 | return (
5 |
6 |
7 |
8 | {error &&
{error}
}
9 |
10 | );
11 | };
12 |
13 | export default Input;
14 |
--------------------------------------------------------------------------------
/vidly/src/components/common/like.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Like = props => {
4 | let classes = "fa fa-heart";
5 | if (!props.liked) classes += "-o";
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default Like;
17 |
--------------------------------------------------------------------------------
/vidly/src/components/common/listGroup.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ListGroup = ({
4 | items,
5 | textProperty,
6 | valueProperty,
7 | selectedItem,
8 | onItemSelect
9 | }) => {
10 | return (
11 |
12 | {items.map(item => (
13 | - onItemSelect(item)}
15 | key={item[valueProperty]}
16 | className={
17 | item === selectedItem ? "list-group-item active" : "list-group-item clickable"
18 | }
19 | >
20 | {item[textProperty]}
21 |
22 | ))}
23 |
24 | );
25 | };
26 |
27 | ListGroup.defaultProps = {
28 | textProperty: "name",
29 | valueProperty: "_id"
30 | };
31 |
32 | export default ListGroup;
33 |
--------------------------------------------------------------------------------
/vidly/src/components/common/pagination.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import _ from "lodash";
4 |
5 | const Pagination = ({ itemsCount, pageSize, currentPage, onPageChange }) => {
6 | const pagesCount = Math.ceil(itemsCount / pageSize);
7 | if (pagesCount === 1) return null;
8 | const pages = _.range(1, pagesCount + 1);
9 |
10 | return (
11 |
25 | );
26 | };
27 |
28 | Pagination.propTypes = {
29 | itemsCount: PropTypes.number.isRequired,
30 | pageSize: PropTypes.number.isRequired,
31 | currentPage: PropTypes.number.isRequired,
32 | onPageChange: PropTypes.func.isRequired
33 | };
34 |
35 | export default Pagination;
36 |
--------------------------------------------------------------------------------
/vidly/src/components/common/protectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Redirect } from "react-router-dom";
3 | import auth from "../../services/authService";
4 |
5 | const ProtectedRoute = ({ path, component: Component, render, ...rest }) => {
6 | return (
7 | {
10 | if (!auth.getCurrentUser())
11 | return (
12 |
18 | );
19 | return Component ? : render(props);
20 | }}
21 | />
22 | );
23 | };
24 |
25 | export default ProtectedRoute;
26 |
--------------------------------------------------------------------------------
/vidly/src/components/common/select.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Select = ({ name, label, options, error, ...rest }) => {
4 | return (
5 |
6 |
7 |
15 | {error &&
{error}
}
16 |
17 | );
18 | };
19 |
20 | export default Select;
21 |
--------------------------------------------------------------------------------
/vidly/src/components/common/table.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TableHeader from "./tableHeader";
3 | import TableBody from "./tableBody";
4 |
5 | const Table = ({ columns, sortColumn, onSort, data }) => {
6 | return (
7 |
11 | );
12 | };
13 |
14 | export default Table;
15 |
--------------------------------------------------------------------------------
/vidly/src/components/common/tableBody.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import _ from "lodash";
3 |
4 | class TableBody extends Component {
5 | renderCell = (item, column) => {
6 | if (column.content) return column.content(item);
7 |
8 | return _.get(item, column.path);
9 | };
10 |
11 | createKey = (item, column) => {
12 | return item._id + (column.path || column.key);
13 | };
14 |
15 | render() {
16 | const { data, columns } = this.props;
17 |
18 | return (
19 |
20 | {data.map(item => (
21 |
22 | {columns.map(column => (
23 |
24 | {this.renderCell(item, column)}
25 | |
26 | ))}
27 |
28 | ))}
29 |
30 | );
31 | }
32 | }
33 |
34 | export default TableBody;
35 |
--------------------------------------------------------------------------------
/vidly/src/components/common/tableHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | // columns: array
4 | // sortColumn: object
5 | // onSort: function
6 |
7 | class TableHeader extends Component {
8 | raiseSort = path => {
9 | const sortColumn = { ...this.props.sortColumn };
10 | if (sortColumn.path === path)
11 | sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
12 | else {
13 | sortColumn.path = path;
14 | sortColumn.order = "asc";
15 | }
16 | this.props.onSort(sortColumn);
17 | };
18 |
19 | renderSortIcon = column => {
20 | const { sortColumn } = this.props;
21 |
22 | if (column.path !== sortColumn.path) return null;
23 | if (sortColumn.order === "asc") return ;
24 | return ;
25 | };
26 |
27 | render() {
28 | return (
29 |
30 |
31 | {this.props.columns.map(column => (
32 | this.raiseSort(column.path)}
36 | >
37 | {column.label} {this.renderSortIcon(column)}
38 | |
39 | ))}
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | export default TableHeader;
47 |
--------------------------------------------------------------------------------
/vidly/src/components/customers.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Customers = () => {
4 | return Customers
;
5 | };
6 |
7 | export default Customers;
8 |
--------------------------------------------------------------------------------
/vidly/src/components/loginForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect } from "react-router-dom";
3 | import Joi from "joi-browser";
4 | import Form from "./common/form";
5 | import auth from "../services/authService";
6 |
7 | class LoginForm extends Form {
8 | state = {
9 | data: { username: "", password: "" },
10 | errors: {}
11 | };
12 |
13 | schema = {
14 | username: Joi.string()
15 | .required()
16 | .label("Username"),
17 | password: Joi.string()
18 | .required()
19 | .label("Password")
20 | };
21 |
22 |
23 | doSubmit = async () => {
24 | try {
25 | const { data } = this.state;
26 | await auth.login(data.username, data.password);
27 |
28 | const { state } = this.props.location;
29 | window.location = state ? state.from.pathname : "/";
30 | } catch (ex) {
31 | if (ex.response && ex.response.status === 400) {
32 | const errors = { ...this.state.errors };
33 | errors.username = ex.response.data;
34 | this.setState({ errors });
35 | }
36 | }
37 | };
38 |
39 | render() {
40 | if (auth.getCurrentUser()) return ;
41 |
42 | return (
43 |
44 |
Login
45 |
50 |
51 | );
52 | }
53 | }
54 |
55 | export default LoginForm;
56 |
--------------------------------------------------------------------------------
/vidly/src/components/logout.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import auth from "../services/authService";
3 |
4 | class Logout extends Component {
5 | componentDidMount() {
6 | auth.logout();
7 |
8 | window.location = "/";
9 | }
10 |
11 | render() {
12 | return null;
13 | }
14 | }
15 |
16 | export default Logout;
17 |
--------------------------------------------------------------------------------
/vidly/src/components/movieForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Joi from "joi-browser";
3 | import Form from "./common/form";
4 | import { getMovie, saveMovie } from "../services/movieService";
5 | import { getGenres } from "../services/genreService";
6 |
7 | class MovieForm extends Form {
8 | state = {
9 | data: {
10 | title: "",
11 | genreId: "",
12 | numberInStock: "",
13 | dailyRentalRate: ""
14 | },
15 | genres: [],
16 | errors: {}
17 | };
18 |
19 | schema = {
20 | _id: Joi.string(),
21 | title: Joi.string()
22 | .required()
23 | .label("Title"),
24 | genreId: Joi.string()
25 | .required()
26 | .label("Genre"),
27 | numberInStock: Joi.number()
28 | .required()
29 | .min(0)
30 | .max(100)
31 | .label("Number in Stock"),
32 | dailyRentalRate: Joi.number()
33 | .required()
34 | .min(0)
35 | .max(10)
36 | .label("Daily Rental Rate")
37 | };
38 |
39 | async populateGenres() {
40 | const { data: genres } = await getGenres();
41 | this.setState({ genres });
42 | }
43 |
44 | async populateMovie() {
45 | try {
46 | const movieId = this.props.match.params.id;
47 | if (movieId === "new") return;
48 |
49 | const { data: movie } = await getMovie(movieId);
50 | this.setState({ data: this.mapToViewModel(movie) });
51 | } catch (ex) {
52 | if (ex.response && ex.response.status === 404)
53 | this.props.history.replace("/not-found");
54 | }
55 | }
56 |
57 | async componentDidMount() {
58 | await this.populateGenres();
59 | await this.populateMovie();
60 | }
61 |
62 | mapToViewModel(movie) {
63 | return {
64 | _id: movie._id,
65 | title: movie.title,
66 | genreId: movie.genre._id,
67 | numberInStock: movie.numberInStock,
68 | dailyRentalRate: movie.dailyRentalRate
69 | };
70 | }
71 |
72 | doSubmit = async () => {
73 | await saveMovie(this.state.data);
74 |
75 | this.props.history.push("/movies");
76 | }
77 |
78 | render() {
79 | return (
80 |
81 |
Movie Form
82 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default MovieForm;
95 |
--------------------------------------------------------------------------------
/vidly/src/components/movies.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import MoviesTable from "./moviesTable";
4 | import ListGroup from "./common/listGroup";
5 | import Pagination from "./common/pagination";
6 | import { paginate } from "../utils/paginate";
7 | import { getMovies, deleteMovie } from "../services/movieService";
8 | import { getGenres } from "../services/genreService";
9 | import SearchBox from "./searchBox";
10 | import _ from "lodash";
11 |
12 | class Movies extends Component {
13 | state = {
14 | movies: [],
15 | genres: [],
16 | currentPage: 1,
17 | pageSize: 4,
18 | searchQuery: "",
19 | selectedGenre: null,
20 | sortColumn: { path: "title", order: "asc" }
21 | };
22 |
23 | async componentDidMount() {
24 | const { data } = await getGenres();
25 | const genres = [{ _id: "", name: "All Genres" }, ...data];
26 |
27 | const { data: movies } = await getMovies();
28 | this.setState({ movies, genres });
29 | }
30 |
31 | handleDelete = movie => {
32 | const movies = this.state.movies.filter(m => m._id !== movie._id);
33 | this.setState({ movies });
34 |
35 | deleteMovie(movie._id);
36 | };
37 |
38 | handleLike = movie => {
39 | const movies = [...this.state.movies];
40 | const index = movies.indexOf(movie);
41 | movies[index] = { ...movies[index] };
42 | movies[index].liked = !movies[index].liked;
43 | this.setState({ movies });
44 | };
45 |
46 | handlePageChange = page => {
47 | this.setState({ currentPage: page });
48 | };
49 |
50 | handleGenreSelect = genre => {
51 | this.setState({ selectedGenre: genre, currentPage: 1 });
52 | };
53 |
54 | handleSearch = query => {
55 | this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 });
56 | };
57 |
58 | handleSort = sortColumn => {
59 | this.setState({ sortColumn });
60 | };
61 |
62 | getPagedData = () => {
63 | const {
64 | pageSize,
65 | currentPage,
66 | sortColumn,
67 | selectedGenre,
68 | searchQuery,
69 | movies: allMovies
70 | } = this.state;
71 |
72 | let filtered = allMovies;
73 | if (searchQuery)
74 | filtered = allMovies.filter(m =>
75 | m.title.toLowerCase().startsWith(searchQuery.toLowerCase())
76 | );
77 | else if (selectedGenre && selectedGenre._id)
78 | filtered = allMovies.filter(m => m.genre._id === selectedGenre._id);
79 |
80 | const sorted = _.orderBy(filtered, [sortColumn.path], [sortColumn.order]);
81 |
82 | const movies = paginate(sorted, currentPage, pageSize);
83 |
84 | return { totalCount: filtered.length, data: movies };
85 | };
86 |
87 | render() {
88 | const { length: count } = this.state.movies;
89 | const { pageSize, currentPage, sortColumn, searchQuery } = this.state;
90 | const { user } = this.props;
91 |
92 | if (count === 0) return There are no movies in the database.
;
93 |
94 | const { totalCount, data: movies } = this.getPagedData();
95 |
96 | return (
97 |
98 |
99 |
104 |
105 |
106 | {user && (
107 |
112 | New Movie
113 | )}
114 |
Showing {totalCount} movies in the database.
115 |
116 |
123 |
129 |
130 |
131 | );
132 | }
133 | }
134 |
135 | export default Movies;
136 |
--------------------------------------------------------------------------------
/vidly/src/components/moviesTable.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import Table from "./common/table";
4 | import Like from "./common/like";
5 | import auth from "../services/authService";
6 |
7 | class MoviesTable extends Component {
8 | columns = [
9 | {
10 | path: "title",
11 | label: "Title",
12 | content: movie => {movie.title}
13 | },
14 | { path: "genre.name", label: "Genre" },
15 | { path: "numberInStock", label: "Stock" },
16 | { path: "dailyRentalRate", label: "Rate" },
17 | {
18 | key: "like",
19 | content: movie => (
20 | this.props.onLike(movie)} />
21 | )
22 | }
23 | ];
24 |
25 | deleteColumn = {
26 | key: "delete",
27 | content: movie => (
28 |
34 | )
35 | };
36 |
37 | constructor() {
38 | super();
39 | const user = auth.getCurrentUser();
40 | if (user && user.isAdmin) this.columns.push(this.deleteColumn);
41 | }
42 |
43 | render() {
44 | const { movies, onSort, sortColumn } = this.props;
45 |
46 | return (
47 |
53 | );
54 | }
55 | }
56 |
57 | export default MoviesTable;
58 |
--------------------------------------------------------------------------------
/vidly/src/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, NavLink } from "react-router-dom";
3 |
4 | const NavBar = ({ user }) => {
5 | return (
6 |
55 | );
56 | };
57 |
58 | export default NavBar;
59 |
--------------------------------------------------------------------------------
/vidly/src/components/notFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotFound = () => {
4 | return Not Found
;
5 | };
6 |
7 | export default NotFound;
8 |
--------------------------------------------------------------------------------
/vidly/src/components/registerForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Joi from "joi-browser";
3 | import Form from "./common/form";
4 | import user from "../services/userService";
5 | import auth from "../services/authService";
6 |
7 | class RegisterForm extends Form {
8 | state = {
9 | data: { username: "", password: "", name: "" },
10 | errors: {}
11 | };
12 |
13 | schema = {
14 | username: Joi.string()
15 | .required()
16 | .email()
17 | .label("Username"),
18 | password: Joi.string()
19 | .required()
20 | .min(5)
21 | .label("Password"),
22 | name: Joi.string()
23 | .required()
24 | .label("Name")
25 | };
26 |
27 | doSubmit = async () => {
28 | try {
29 | const response = await user.register(this.state.data);
30 | auth.loginWithJwt(response.headers["x-auth-token"]);
31 | window.location = "/";
32 | } catch (ex) {
33 | if (ex.response && ex.response.status === 400) {
34 | const errors = { ...this.state.errors };
35 | errors.username = ex.response.data;
36 | this.setState({ errors });
37 | }
38 | }
39 | };
40 |
41 | render() {
42 | return (
43 |
44 |
Register
45 |
51 |
52 | );
53 | }
54 | }
55 |
56 | export default RegisterForm;
57 |
--------------------------------------------------------------------------------
/vidly/src/components/rentals.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Rentals = () => {
4 | return Rentals
;
5 | };
6 |
7 | export default Rentals;
8 |
--------------------------------------------------------------------------------
/vidly/src/components/searchBox.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SearchBox = ({ value, onChange }) => {
4 | return (
5 | onChange(e.currentTarget.value)}
12 | />
13 | );
14 | };
15 |
16 | export default SearchBox;
17 |
--------------------------------------------------------------------------------
/vidly/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiUrl": "http://localhost:3900/api"
3 | }
4 |
--------------------------------------------------------------------------------
/vidly/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0 0 0 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | .navbar {
8 | margin-bottom: 30px;
9 | }
10 |
11 | .clickable {
12 | cursor: pointer;
13 | }
--------------------------------------------------------------------------------
/vidly/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 | import App from "./App";
5 | import registerServiceWorker from "./registerServiceWorker";
6 | import "./index.css";
7 | import "bootstrap/dist/css/bootstrap.css";
8 | import "font-awesome/css/font-awesome.css";
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById("root")
15 | );
16 | registerServiceWorker();
17 |
--------------------------------------------------------------------------------
/vidly/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/vidly/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/vidly/src/services/authService.js:
--------------------------------------------------------------------------------
1 | import jwtDecode from "jwt-decode";
2 | import http from "./httpService";
3 | import { apiUrl } from "../config.json";
4 |
5 | const apiEndpoint = apiUrl + "/auth";
6 | const tokenKey = "token";
7 |
8 | http.setJwt(getJwt());
9 |
10 | export async function login(email, password) {
11 | const { data: jwt } = await http.post(apiEndpoint, { email, password });
12 | localStorage.setItem(tokenKey, jwt);
13 | }
14 |
15 | export function loginWithJwt(jwt) {
16 | localStorage.setItem(tokenKey, jwt);
17 | }
18 |
19 | export function logout() {
20 | localStorage.removeItem(tokenKey);
21 | }
22 |
23 | export function getCurrentUser() {
24 | try {
25 | const jwt = localStorage.getItem(tokenKey);
26 | return jwtDecode(jwt);
27 | } catch (ex) {
28 | return null;
29 | }
30 | }
31 |
32 | export function getJwt() {
33 | return localStorage.getItem(tokenKey);
34 | }
35 |
36 | export default {
37 | login,
38 | loginWithJwt,
39 | logout,
40 | getCurrentUser,
41 | getJwt
42 | };
43 |
--------------------------------------------------------------------------------
/vidly/src/services/genreService.js:
--------------------------------------------------------------------------------
1 | import http from "./httpService";
2 | import { apiUrl } from "../config.json";
3 |
4 | export function getGenres() {
5 | return http.get(apiUrl + "/genres");
6 | }
7 |
--------------------------------------------------------------------------------
/vidly/src/services/httpService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import logger from "./logService";
3 | import { toast } from "react-toastify";
4 |
5 | axios.interceptors.response.use(null, error => {
6 | const expectedError =
7 | error.response &&
8 | error.response.status >= 400 &&
9 | error.response.status < 500;
10 |
11 | if (!expectedError) {
12 | logger.log(error);
13 | toast.error("An unexpected error occurrred.");
14 | }
15 |
16 | return Promise.reject(error);
17 | });
18 |
19 | function setJwt(jwt) {
20 | axios.defaults.headers.common["x-auth-token"] = jwt;
21 | }
22 |
23 | export default {
24 | get: axios.get,
25 | post: axios.post,
26 | put: axios.put,
27 | delete: axios.delete,
28 | setJwt
29 | };
30 |
--------------------------------------------------------------------------------
/vidly/src/services/logService.js:
--------------------------------------------------------------------------------
1 | // For simplicity, I changed the implementation of this module
2 | // and removed Raven. We can always add that in the future
3 | // and this module is the only module we need to modify.
4 |
5 | function init() {}
6 |
7 | function log(error) {
8 | console.error(error);
9 | }
10 |
11 | export default {
12 | init,
13 | log
14 | };
15 |
--------------------------------------------------------------------------------
/vidly/src/services/movieService.js:
--------------------------------------------------------------------------------
1 | import http from "./httpService";
2 | import { apiUrl } from "../config.json";
3 |
4 | const apiEndpoint = apiUrl + "/movies";
5 |
6 | function movieUrl(id) {
7 | return `${apiEndpoint}/${id}`;
8 | }
9 |
10 | export function getMovies() {
11 | return http.get(apiEndpoint);
12 | }
13 |
14 | export function getMovie(movieId) {
15 | return http.get(movieUrl(movieId));
16 | }
17 |
18 | export function saveMovie(movie) {
19 | if (movie._id) {
20 | const body = { ...movie };
21 | delete body._id;
22 | return http.put(movieUrl(movie._id), body);
23 | }
24 |
25 | return http.post(apiEndpoint, movie);
26 | }
27 |
28 | export function deleteMovie(movieId) {
29 | return http.delete(movieUrl(movieId));
30 | }
31 |
--------------------------------------------------------------------------------
/vidly/src/services/userService.js:
--------------------------------------------------------------------------------
1 | import http from "./httpService";
2 | import { apiUrl } from "../config.json";
3 |
4 | const apiEndpoint = apiUrl + "/users";
5 |
6 | export function register(user) {
7 | return http.post(apiEndpoint, {
8 | email: user.username,
9 | password: user.password,
10 | name: user.name
11 | });
12 |
13 |
14 | }
15 |
16 | export default {
17 | register
18 | };
--------------------------------------------------------------------------------
/vidly/src/utils/paginate.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export function paginate(items, pageNumber, pageSize) {
4 | const startIndex = (pageNumber - 1) * pageSize;
5 | return _(items)
6 | .slice(startIndex)
7 | .take(pageSize)
8 | .value();
9 | }
10 |
--------------------------------------------------------------------------------