├── 01-pure-to-jsx ├── _start │ └── index.js └── finish │ └── index.js ├── 02-state-clicker ├── _start │ └── App.jsx └── finish │ └── App.jsx ├── 03-lifecicle-timer ├── _start │ └── App.jsx └── finish │ └── App.jsx ├── 04-fn-flow ├── _start │ ├── App.jsx │ ├── Post.jsx │ └── Posts.jsx └── finish │ ├── App.jsx │ ├── Post.jsx │ └── Posts.jsx ├── 05-form-subscription ├── _start │ └── SubscriptionForm.jsx └── finish │ └── SubscriptionForm.jsx ├── 06-movies-list ├── _start │ ├── Main.jsx │ ├── Movie.jsx │ └── Movies.jsx └── finish │ ├── Main.jsx │ ├── Movie.jsx │ └── Movies.jsx ├── 07-movies-search ├── _start │ ├── Main.jsx │ └── Search.jsx └── finish │ ├── Main.jsx │ └── Search.jsx ├── 08-movies-filter ├── _start │ ├── Main.jsx │ └── Search.jsx └── finish │ ├── Main.jsx │ └── Search.jsx ├── 09-clicker-fn ├── _start │ └── App.jsx └── finish │ └── App.jsx ├── 10-timer-fn ├── _start │ └── App.js └── finish │ └── App.js ├── 11-movies-refactor ├── _start │ └── Main.jsx └── finish │ └── Main.jsx ├── 12-shop-add-item ├── _start │ └── Shop.jsx └── finish │ └── Shop.jsx ├── 13-shop-change-quantity ├── _start │ └── Shop.jsx └── finish │ └── Shop.jsx ├── 14-reducer-practice ├── _start │ ├── context.js │ └── reducer.js └── finish │ ├── context.js │ └── reducer.js └── README.md /01-pure-to-jsx/_start/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | // tip: чтобы встроить переменную в JSX используйте фигурные скобки {name} 5 | const Book = (props) => { 6 | return React.createElement("div", {}, [ 7 | React.createElement("h2", {}, props.name), 8 | React.createElement("p", {}, props.year), 9 | React.createElement("p", {}, props.price) 10 | ]); 11 | }; 12 | 13 | const App = () => { 14 | return ( 15 |
16 | 17 | 18 | 19 |
20 | ); 21 | 22 | // React.createElement("div", {}, [ 23 | // React.createElement( 24 | // "h1", 25 | // { id: "hello", className: "class1" }, 26 | // "Hello from React" 27 | // ), 28 | // React.createElement(Book, { 29 | // name: "JS for beginners", 30 | // year: 2018, 31 | // price: 1000 32 | // }), 33 | // React.createElement(Book, { name: "React", year: 2020, price: 1200 }), 34 | // React.createElement(Book, { name: "Vue JS", year: 2019, price: 1100 }) 35 | // ]); 36 | }; 37 | 38 | 39 | const rootElement = document.getElementById("root"); 40 | ReactDOM.render( 41 | 42 | 43 | , 44 | 45 | rootElement 46 | ); 47 | -------------------------------------------------------------------------------- /01-pure-to-jsx/finish/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | 5 | const Book = (props) => { 6 | return
7 |

{props.name}

8 |

{props.year}

9 |

{props.price}

10 |
11 | 12 | // React.createElement("div", {}, [ 13 | // React.createElement("h2", {}, props.name), 14 | // React.createElement("p", {}, props.year), 15 | // React.createElement("p", {}, props.price) 16 | // ]); 17 | }; 18 | 19 | const App = () => { 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | 30 | const rootElement = document.getElementById("root"); 31 | ReactDOM.render( 32 | 33 | 34 | , 35 | 36 | rootElement 37 | ); 38 | -------------------------------------------------------------------------------- /02-state-clicker/_start/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | state = { 5 | count: 0, 6 | }; 7 | 8 | handleClick = () => { 9 | this.setState({ count: this.state.count + 1 }); 10 | }; 11 | 12 | render() { 13 | return ( 14 |
15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /02-state-clicker/finish/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | state = { 5 | count: 0, 6 | }; 7 | 8 | increment = () => { 9 | this.setState({ count: this.state.count + 1 }); 10 | }; 11 | 12 | decrement = () => { 13 | this.setState({ count: this.state.count - 1 }); 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 | 20 | {this.state.count} 21 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /03-lifecicle-timer/_start/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles.css'; 3 | 4 | export default class App extends React.Component { 5 | state = { 6 | count: 0, 7 | isCounting: false, 8 | }; 9 | 10 | componentDidMount() {} 11 | 12 | componentDidUpdate() {} 13 | 14 | componentWillUnmount() {} 15 | 16 | render() { 17 | return ( 18 |
19 |

React Timer

20 |

{this.state.count}

21 | {!this.state.isCounting ? ( 22 | 23 | ) : ( 24 | 25 | )} 26 | 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /03-lifecicle-timer/finish/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles.css'; 3 | 4 | export default class App extends React.Component { 5 | state = { 6 | count: 0, 7 | isCounting: false, 8 | }; 9 | 10 | componentDidMount() { 11 | const userCount = localStorage.getItem('count'); 12 | if (userCount) this.setState({ count: +userCount }); 13 | } 14 | 15 | componentDidUpdate() { 16 | localStorage.setItem('count', this.state.count); 17 | } 18 | 19 | handleReset = () => { 20 | this.setState({ 21 | count: 0, 22 | isCounting: false, 23 | }); 24 | }; 25 | 26 | handleStart = () => { 27 | this.setState({ 28 | isCounting: true, 29 | }); 30 | this.counterId = setInterval(() => { 31 | this.setState({ count: this.state.count + 1 }); 32 | }, 1000); 33 | }; 34 | 35 | handleStop = () => { 36 | clearInterval(this.counterId); 37 | this.setState({ 38 | isCounting: false, 39 | }); 40 | }; 41 | 42 | componentWillUnmount() { 43 | clearInterval(this.counterId); 44 | this.handleStop(); 45 | } 46 | 47 | render() { 48 | return ( 49 |
50 |

React Timer

51 |

{this.state.count}

52 | {!this.state.isCounting ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /04-fn-flow/_start/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Posts } from './components/Posts'; 3 | 4 | class App extends Component { 5 | state = { 6 | posts: [ 7 | { id: 'abc1', name: 'JS Basics' }, 8 | { id: 'abc2', name: 'JS Advanced' }, 9 | { id: 'abc3', name: 'Recat JS' }, 10 | ], 11 | }; 12 | 13 | render() { 14 | const { posts } = this.state; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /04-fn-flow/_start/Post.jsx: -------------------------------------------------------------------------------- 1 | export function Post(props) { 2 | const { name } = props; 3 | 4 | return ( 5 |

6 | {name} 7 |

8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /04-fn-flow/_start/Posts.jsx: -------------------------------------------------------------------------------- 1 | import { Post } from './Post'; 2 | 3 | export function Posts(props) { 4 | return ( 5 |
6 | {props.posts.map((post) => ( 7 | 8 | ))} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /04-fn-flow/finish/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Posts } from './Posts'; 3 | 4 | class App extends Component { 5 | state = { 6 | posts: [ 7 | { id: 'abc1', name: 'JS Basics' }, 8 | { id: 'abc2', name: 'JS Advanced' }, 9 | { id: 'abc3', name: 'Recat JS' }, 10 | ], 11 | }; 12 | 13 | removePost = (id) => { 14 | this.setState({ 15 | posts: this.state.posts.filter((post) => post.id !== id), 16 | }); 17 | }; 18 | 19 | render() { 20 | const { posts } = this.state; 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /04-fn-flow/finish/Post.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function Post(props) { 4 | const { id, name, removePost } = props; 5 | 6 | return ( 7 |

8 | {name} 9 |

10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /04-fn-flow/finish/Posts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Post } from './Post'; 3 | 4 | export function Posts(props) { 5 | return ( 6 |
7 | {props.posts.map((post) => ( 8 | 14 | ))} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /05-form-subscription/_start/SubscriptionForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SubscriptionForm extends React.Component { 4 | state = { 5 | email: '', 6 | isAgreeWithTerms: false, 7 | }; 8 | 9 | render() { 10 | const { email, isAgreeWithTerms } = this.state; 11 | 12 | return ( 13 |
14 | 20 |
21 | 29 |
30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export { SubscriptionForm }; 37 | -------------------------------------------------------------------------------- /05-form-subscription/finish/SubscriptionForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SubscriptionForm extends React.Component { 4 | state = { 5 | email: '', 6 | isAgreeWithTerms: false, 7 | }; 8 | 9 | handleEmail = (event) => { 10 | this.setState({ email: event.target.value }); 11 | }; 12 | 13 | handleCheckbox = (event) => { 14 | this.setState({ 15 | isAgreeWithTerms: event.target.checked, 16 | }); 17 | }; 18 | 19 | handleSubmit = () => { 20 | const isValidEmail = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( 21 | this.state.email.toLocaleLowerCase() 22 | ); 23 | const isValidCheckbox = this.state.isAgreeWithTerms; 24 | 25 | if (!isValidEmail) { 26 | alert('Your email is not valid'); 27 | return; 28 | } 29 | 30 | if (!isValidCheckbox) { 31 | alert('You should accept all terms and conditions'); 32 | return; 33 | } 34 | 35 | this.setState({ 36 | email: '', 37 | isAgreeWithTerms: false, 38 | }); 39 | alert('Thank you for subscription!'); 40 | }; 41 | 42 | render() { 43 | const { email, isAgreeWithTerms } = this.state; 44 | 45 | return ( 46 |
47 | 54 |
55 | 64 |
65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | export { SubscriptionForm }; 72 | -------------------------------------------------------------------------------- /06-movies-list/_start/Main.jsx: -------------------------------------------------------------------------------- 1 | function Main() { 2 | return
; 3 | } 4 | 5 | export { Main }; 6 | -------------------------------------------------------------------------------- /06-movies-list/_start/Movie.jsx: -------------------------------------------------------------------------------- 1 | function Movie() { 2 | return
; 3 | } 4 | 5 | export { Movie }; 6 | -------------------------------------------------------------------------------- /06-movies-list/_start/Movies.jsx: -------------------------------------------------------------------------------- 1 | function Movies() { 2 | return
; 3 | } 4 | 5 | export { Movies }; 6 | -------------------------------------------------------------------------------- /06-movies-list/finish/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | 4 | class Main extends React.Component { 5 | state = { 6 | movies: [], 7 | }; 8 | 9 | componentDidMount() { 10 | fetch('http://www.omdbapi.com/?apikey=78584b3c&s=matrix') 11 | .then((response) => response.json()) 12 | .then((data) => this.setState({ movies: data.Search })); 13 | } 14 | 15 | render() { 16 | const { movies } = this.state; 17 | 18 | return ( 19 |
20 | {movies.length ? ( 21 | 22 | ) : ( 23 |

Loading...

24 | )} 25 |
26 | ); 27 | } 28 | } 29 | 30 | export { Main }; 31 | -------------------------------------------------------------------------------- /06-movies-list/finish/Movie.jsx: -------------------------------------------------------------------------------- 1 | function Movie(props) { 2 | const { 3 | Title: title, 4 | Year: year, 5 | imdbID: id, 6 | Type: type, 7 | Poster: poster, 8 | } = props; 9 | 10 | console.log('movie render'); 11 | 12 | return ( 13 |
14 |
15 | {poster === 'N/A' ? ( 16 | 20 | ) : ( 21 | 22 | )} 23 |
24 |
25 | 26 | {title} 27 | 28 |

29 | {year} {type} 30 |

31 |
32 |
33 | ); 34 | } 35 | export { Movie }; 36 | -------------------------------------------------------------------------------- /06-movies-list/finish/Movies.jsx: -------------------------------------------------------------------------------- 1 | import { Movie } from './Movie'; 2 | 3 | function Movies(props) { 4 | const { movies } = props; 5 | 6 | return ( 7 |
8 | {movies.map((movie) => ( 9 | 10 | ))} 11 |
12 | ); 13 | } 14 | export { Movies }; 15 | -------------------------------------------------------------------------------- /07-movies-search/_start/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | import { Search } from '../components/Search'; 4 | import { Preloader } from '../components/Preloader'; 5 | 6 | class Main extends React.Component { 7 | state = { 8 | movies: [], 9 | }; 10 | 11 | componentDidMount() { 12 | fetch('http://www.omdbapi.com/?apikey=78584b3c&s=matrix') 13 | .then((response) => response.json()) 14 | .then((data) => this.setState({ movies: data.Search })); 15 | } 16 | 17 | render() { 18 | const { movies } = this.state; 19 | 20 | return ( 21 |
22 | 23 | {movies.length ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 |
29 | ); 30 | } 31 | } 32 | 33 | export { Main }; 34 | -------------------------------------------------------------------------------- /07-movies-search/_start/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Search extends React.Component { 4 | state = { 5 | search: '', 6 | }; 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | 18 | this.setState({ search: e.target.value }) 19 | } 20 | /> 21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | export { Search }; 28 | -------------------------------------------------------------------------------- /07-movies-search/finish/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | import { Search } from '../components/Search'; 4 | import { Preloader } from '../components/Preloader'; 5 | 6 | class Main extends React.Component { 7 | state = { 8 | movies: [], 9 | }; 10 | 11 | searchMovies = (str) => { 12 | fetch(`http://www.omdbapi.com/?apikey=78584b3c&s=${str}`) 13 | .then((response) => response.json()) 14 | .then((data) => this.setState({ movies: data.Search })); 15 | }; 16 | 17 | componentDidMount() { 18 | fetch('http://www.omdbapi.com/?apikey=78584b3c&s=matrix') 19 | .then((response) => response.json()) 20 | .then((data) => this.setState({ movies: data.Search })); 21 | } 22 | 23 | render() { 24 | const { movies } = this.state; 25 | 26 | return ( 27 |
28 | 29 | {movies.length ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 |
35 | ); 36 | } 37 | } 38 | 39 | export { Main }; 40 | -------------------------------------------------------------------------------- /07-movies-search/finish/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Search extends React.Component { 4 | state = { 5 | search: '', 6 | }; 7 | 8 | handleKey = (event) => { 9 | if (event.key === 'Enter') { 10 | this.props.searchMovies(this.state.search); 11 | } 12 | }; 13 | 14 | render() { 15 | return ( 16 |
17 |
18 | 24 | this.setState({ search: e.target.value }) 25 | } 26 | onKeyDown={this.handleKey} 27 | /> 28 | 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export { Search }; 43 | -------------------------------------------------------------------------------- /08-movies-filter/_start/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | import { Search } from '../components/Search'; 4 | import { Preloader } from '../components/Preloader'; 5 | 6 | class Main extends React.Component { 7 | state = { 8 | movies: [], 9 | }; 10 | 11 | componentDidMount() { 12 | fetch('http://www.omdbapi.com/?apikey=78584b3c&s=matrix') 13 | .then((response) => response.json()) 14 | .then((data) => this.setState({ movies: data.Search })); 15 | } 16 | 17 | searchMovies = (str) => { 18 | fetch(`http://www.omdbapi.com/?apikey=78584b3c&s=${str}`) 19 | .then((response) => response.json()) 20 | .then((data) => this.setState({ movies: data.Search })); 21 | }; 22 | 23 | render() { 24 | const { movies } = this.state; 25 | 26 | return ( 27 |
28 | 29 | {movies.length ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 |
35 | ); 36 | } 37 | } 38 | 39 | export { Main }; 40 | -------------------------------------------------------------------------------- /08-movies-filter/_start/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Search extends React.Component { 4 | state = { 5 | search: '', 6 | }; 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | 18 | this.setState({ search: e.target.value }) 19 | } 20 | /> 21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | export { Search }; 28 | -------------------------------------------------------------------------------- /08-movies-filter/finish/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | import { Search } from '../components/Search'; 4 | import { Preloader } from '../components/Preloader'; 5 | 6 | class Main extends React.Component { 7 | state = { 8 | movies: [], 9 | }; 10 | 11 | componentDidMount() { 12 | fetch('http://www.omdbapi.com/?apikey=78584b3c&s=matrix') 13 | .then((response) => response.json()) 14 | .then((data) => this.setState({ movies: data.Search })); 15 | } 16 | 17 | searchMovies = (str, type = 'all') => { 18 | fetch( 19 | `http://www.omdbapi.com/?apikey=78584b3c&s=${str}${ 20 | type !== 'all' ? `&type=${type}` : '' 21 | }` 22 | ) 23 | .then((response) => response.json()) 24 | .then((data) => this.setState({ movies: data.Search })); 25 | }; 26 | 27 | render() { 28 | const { movies } = this.state; 29 | 30 | return ( 31 |
32 | 33 | {!movies.length ? : } 34 |
35 | ); 36 | } 37 | } 38 | 39 | export { Main }; 40 | -------------------------------------------------------------------------------- /08-movies-filter/finish/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Search extends React.Component { 4 | state = { 5 | search: '', 6 | type: 'all', 7 | }; 8 | 9 | handleKey = (event) => { 10 | if (event.key === 'Enter') { 11 | this.props.searchMovies(this.state.search, this.state.type); 12 | } 13 | }; 14 | 15 | handleFilter = (event) => { 16 | this.setState( 17 | () => ({ type: event.target.dataset.type }), 18 | () => { 19 | this.props.searchMovies(this.state.search, this.state.type); 20 | } 21 | ); 22 | }; 23 | 24 | render() { 25 | return ( 26 |
27 |
28 | 34 | this.setState({ search: e.target.value }) 35 | } 36 | onKeyDown={this.handleKey} 37 | /> 38 | 49 |
50 |
51 | 62 | 73 | 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | export { Search }; 91 | -------------------------------------------------------------------------------- /09-clicker-fn/_start/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | state = { 5 | count: 0, 6 | }; 7 | 8 | increment = () => { 9 | this.setState({ count: this.state.count + 1 }); 10 | }; 11 | 12 | decrement = () => { 13 | this.setState({ count: this.state.count - 1 }); 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 | 20 | {this.state.count} 21 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /09-clicker-fn/finish/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function App() { 4 | const [count, setCount] = useState(0); 5 | 6 | const increment = () => { 7 | setCount(count + 1); 8 | }; 9 | 10 | const decrement = () => { 11 | setCount(count - 1); 12 | }; 13 | 14 | return ( 15 |
16 | 17 | 18 | {count} 19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /10-timer-fn/_start/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class App extends React.Component { 4 | state = { 5 | count: 0, 6 | isCounting: false, 7 | }; 8 | 9 | componentDidMount() { 10 | const userCount = localStorage.getItem('count'); 11 | if (userCount) this.setState({ count: +userCount }); 12 | } 13 | 14 | componentDidUpdate() { 15 | localStorage.setItem('count', this.state.count); 16 | } 17 | 18 | handleReset = () => { 19 | this.setState({ 20 | count: 0, 21 | isCounting: false, 22 | }); 23 | }; 24 | 25 | handleStart = () => { 26 | this.setState({ 27 | isCounting: true, 28 | }); 29 | this.counterId = setInterval(() => { 30 | this.setState({ count: this.state.count + 1 }); 31 | }, 1000); 32 | }; 33 | 34 | handleStop = () => { 35 | clearInterval(this.counterId); 36 | this.setState({ 37 | isCounting: false, 38 | }); 39 | }; 40 | 41 | componentWillUnmount() { 42 | clearInterval(this.counterId); 43 | this.handleStop(); 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 |

React Timer

50 |

{this.state.count}

51 | {!this.state.isCounting ? ( 52 | 53 | ) : ( 54 | 55 | )} 56 | 57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /10-timer-fn/finish/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | 3 | function setDefaultValue() { 4 | const userCount = localStorage.getItem('count'); 5 | return userCount ? +userCount : 0; 6 | } 7 | 8 | export default function Timer() { 9 | const [count, setCount] = useState(setDefaultValue()); 10 | const [isCounting, setIsCount] = useState(false); 11 | const timerIdRef = useRef(null); 12 | 13 | const handleReset = () => { 14 | setCount(0); 15 | setIsCount(false); 16 | }; 17 | 18 | const handleStart = () => { 19 | setIsCount(true); 20 | }; 21 | 22 | const handleStop = () => { 23 | setIsCount(false); 24 | }; 25 | 26 | useEffect(() => { 27 | localStorage.setItem('count', count); 28 | }, [count]); 29 | 30 | useEffect(() => { 31 | if (isCounting) { 32 | timerIdRef.current = setInterval(() => { 33 | setCount((prevCount) => prevCount + 1); 34 | }, 1000); 35 | } 36 | 37 | return () => { 38 | timerIdRef.current && clearInterval(timerIdRef.current); 39 | timerIdRef.current = null; 40 | }; 41 | }, [isCounting]); 42 | 43 | return ( 44 |
45 |

React Timer

46 |

{count}

47 | {!isCounting ? ( 48 | 49 | ) : ( 50 | 51 | )} 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /11-movies-refactor/_start/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Movies } from '../components/Movies'; 3 | import { Search } from '../components/Search'; 4 | import { Preloader } from '../components/Preloader'; 5 | 6 | const API_KEY = process.env.REACT_APP_API_KEY; 7 | 8 | class Main extends React.Component { 9 | state = { 10 | movies: [], 11 | loading: true, 12 | }; 13 | 14 | componentDidMount() { 15 | console.log(process.env); 16 | fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&s=matrix`) 17 | .then((response) => response.json()) 18 | .then((data) => 19 | this.setState({ movies: data.Search, loading: false }) 20 | ) 21 | .catch((err) => { 22 | console.error(err); 23 | this.setState({ loading: false }); 24 | }); 25 | } 26 | 27 | searchMovies = (str, type = 'all') => { 28 | this.setState({ loading: true }); 29 | fetch( 30 | `https://www.omdbapi.com/?apikey=${API_KEY}&s=${str}${ 31 | type !== 'all' ? `&type=${type}` : '' 32 | }` 33 | ) 34 | .then((response) => response.json()) 35 | .then((data) => 36 | this.setState({ movies: data.Search, loading: false }) 37 | ) 38 | .catch((err) => { 39 | console.error(err); 40 | this.setState({ loading: false }); 41 | }); 42 | }; 43 | 44 | render() { 45 | const { movies, loading } = this.state; 46 | 47 | return ( 48 |
49 | 50 | {loading ? : } 51 |
52 | ); 53 | } 54 | } 55 | 56 | export { Main }; 57 | -------------------------------------------------------------------------------- /11-movies-refactor/finish/Main.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michey85/react-course-practice/355488a228b0d36e80f8857368662117491ebca6/11-movies-refactor/finish/Main.jsx -------------------------------------------------------------------------------- /12-shop-add-item/_start/Shop.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { API_KEY, API_URL } from '../config'; 3 | 4 | import { Preloader } from './Preloader'; 5 | import { GoodsList } from './GoodsList'; 6 | import { Cart } from './Cart'; 7 | 8 | function Shop() { 9 | const [goods, setGoods] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | const [order, setOrder] = useState([]); 12 | 13 | const addToBasket = (item) => {}; 14 | 15 | useEffect(function getGoods() { 16 | fetch(API_URL, { 17 | headers: { 18 | Authorization: API_KEY, 19 | }, 20 | }) 21 | .then((response) => response.json()) 22 | .then((data) => { 23 | data.featured && setGoods(data.featured); 24 | setLoading(false); 25 | }); 26 | }, []); 27 | 28 | return ( 29 |
30 | 31 | {loading ? ( 32 | 33 | ) : ( 34 | 35 | )} 36 |
37 | ); 38 | } 39 | 40 | export { Shop }; 41 | -------------------------------------------------------------------------------- /12-shop-add-item/finish/Shop.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { API_KEY, API_URL } from '../config'; 3 | 4 | import { Preloader } from './Preloader'; 5 | import { GoodsList } from './GoodsList'; 6 | import { Cart } from './Cart'; 7 | 8 | function Shop() { 9 | const [goods, setGoods] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | const [order, setOrder] = useState([]); 12 | 13 | const addToBasket = (item) => { 14 | const itemIndex = order.findIndex( 15 | (orderItem) => orderItem.id === item.id 16 | ); 17 | 18 | if (itemIndex < 0) { 19 | const newItem = { 20 | ...item, 21 | quantity: 1, 22 | }; 23 | setOrder([...order, newItem]); 24 | } else { 25 | const newOrder = order.map((orderItem, index) => { 26 | if (index === itemIndex) { 27 | return { 28 | ...orderItem, 29 | quantity: orderItem.quantity + 1, 30 | }; 31 | } else { 32 | return orderItem; 33 | } 34 | }); 35 | 36 | setOrder(newOrder); 37 | } 38 | }; 39 | 40 | useEffect(function getGoods() { 41 | fetch(API_URL, { 42 | headers: { 43 | Authorization: API_KEY, 44 | }, 45 | }) 46 | .then((response) => response.json()) 47 | .then((data) => { 48 | data.featured && setGoods(data.featured); 49 | setLoading(false); 50 | }); 51 | }, []); 52 | 53 | return ( 54 |
55 | 56 | {loading ? ( 57 | 58 | ) : ( 59 | 60 | )} 61 |
62 | ); 63 | } 64 | 65 | export { Shop }; 66 | -------------------------------------------------------------------------------- /13-shop-change-quantity/_start/Shop.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { API_KEY, API_URL } from '../config'; 3 | 4 | import { Preloader } from './Preloader'; 5 | import { GoodsList } from './GoodsList'; 6 | import { Cart } from './Cart'; 7 | import { BasketList } from './BasketList'; 8 | 9 | function Shop() { 10 | const [goods, setGoods] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | const [order, setOrder] = useState([]); 13 | const [isBasketShow, setBasketSow] = useState(false); 14 | 15 | const addToBasket = (item) => { 16 | const itemIndex = order.findIndex( 17 | (orderItem) => orderItem.id === item.id 18 | ); 19 | 20 | if (itemIndex < 0) { 21 | const newItem = { 22 | ...item, 23 | quantity: 1, 24 | }; 25 | setOrder([...order, newItem]); 26 | } else { 27 | const newOrder = order.map((orderItem, index) => { 28 | if (index === itemIndex) { 29 | return { 30 | ...orderItem, 31 | quantity: orderItem.quantity + 1, 32 | }; 33 | } else { 34 | return orderItem; 35 | } 36 | }); 37 | 38 | setOrder(newOrder); 39 | } 40 | setAlertName(item.name); 41 | }; 42 | 43 | const removeFromBasket = (itemId) => { 44 | const newOrder = order.filter((el) => el.id !== itemId); 45 | setOrder(newOrder); 46 | }; 47 | 48 | const incQuantity = (itemId) => {}; 49 | const decQuantity = (itemId) => {}; 50 | 51 | const handleBasketShow = () => { 52 | setBasketSow(!isBasketShow); 53 | }; 54 | 55 | useEffect(function getGoods() { 56 | fetch(API_URL, { 57 | headers: { 58 | Authorization: API_KEY, 59 | }, 60 | }) 61 | .then((response) => response.json()) 62 | .then((data) => { 63 | data.featured && setGoods(data.featured); 64 | setLoading(false); 65 | }); 66 | }, []); 67 | 68 | return ( 69 |
70 | 71 | {loading ? ( 72 | 73 | ) : ( 74 | 75 | )} 76 | {isBasketShow && ( 77 | 82 | )} 83 |
84 | ); 85 | } 86 | 87 | export { Shop }; 88 | -------------------------------------------------------------------------------- /13-shop-change-quantity/finish/Shop.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { API_KEY, API_URL } from '../config'; 3 | 4 | import { Preloader } from './Preloader'; 5 | import { GoodsList } from './GoodsList'; 6 | import { Cart } from './Cart'; 7 | import { BasketList } from './BasketList'; 8 | 9 | function Shop() { 10 | const [goods, setGoods] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | const [order, setOrder] = useState([]); 13 | const [isBasketShow, setBasketSow] = useState(false); 14 | 15 | const addToBasket = (item) => { 16 | const itemIndex = order.findIndex( 17 | (orderItem) => orderItem.id === item.id 18 | ); 19 | 20 | if (itemIndex < 0) { 21 | const newItem = { 22 | ...item, 23 | quantity: 1, 24 | }; 25 | setOrder([...order, newItem]); 26 | } else { 27 | const newOrder = order.map((orderItem, index) => { 28 | if (index === itemIndex) { 29 | return { 30 | ...orderItem, 31 | quantity: orderItem.quantity + 1, 32 | }; 33 | } else { 34 | return orderItem; 35 | } 36 | }); 37 | 38 | setOrder(newOrder); 39 | } 40 | setAlertName(item.name); 41 | }; 42 | 43 | const removeFromBasket = (itemId) => { 44 | const newOrder = order.filter((el) => el.id !== itemId); 45 | setOrder(newOrder); 46 | }; 47 | 48 | const incQuantity = (itemId) => { 49 | const newOrder = order.map((el) => { 50 | if (el.id === itemId) { 51 | const newQuantity = el.quantity + 1; 52 | return { 53 | ...el, 54 | quantity: newQuantity, 55 | }; 56 | } else { 57 | return el; 58 | } 59 | }); 60 | setOrder(newOrder); 61 | }; 62 | const decQuantity = (itemId) => { 63 | const newOrder = order.map((el) => { 64 | if (el.id === itemId) { 65 | const newQuantity = el.quantity - 1; 66 | return { 67 | ...el, 68 | quantity: newQuantity >= 0 ? newQuantity : 0, 69 | }; 70 | } else { 71 | return el; 72 | } 73 | }); 74 | setOrder(newOrder); 75 | }; 76 | 77 | const handleBasketShow = () => { 78 | setBasketSow(!isBasketShow); 79 | }; 80 | 81 | useEffect(function getGoods() { 82 | fetch(API_URL, { 83 | headers: { 84 | Authorization: API_KEY, 85 | }, 86 | }) 87 | .then((response) => response.json()) 88 | .then((data) => { 89 | data.featured && setGoods(data.featured); 90 | setLoading(false); 91 | }); 92 | }, []); 93 | 94 | return ( 95 |
96 | 97 | {loading ? ( 98 | 99 | ) : ( 100 | 101 | )} 102 | {isBasketShow && ( 103 | 110 | )} 111 |
112 | ); 113 | } 114 | 115 | export { Shop }; 116 | -------------------------------------------------------------------------------- /14-reducer-practice/_start/context.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer } from 'react'; 2 | import { reducer } from './reducer'; 3 | 4 | export const ShopContext = createContext(); 5 | 6 | const initialState = { 7 | goods: [], 8 | loading: true, 9 | order: [], 10 | isBasketShow: false, 11 | alertName: '', 12 | }; 13 | 14 | export const ContextProvider = ({ children }) => { 15 | const [value, dispatch] = useReducer(reducer, initialState); 16 | 17 | value.closeAlert = () => { 18 | dispatch({ type: 'CLOSE_ALERT' }); 19 | }; 20 | 21 | value.removeFromBasket = (itemId) => { 22 | dispatch({ type: 'REMOVE_FROM_BASKET', payload: { id: itemId } }); 23 | }; 24 | 25 | return ( 26 | {children} 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /14-reducer-practice/_start/reducer.js: -------------------------------------------------------------------------------- 1 | export function reducer(state, { type, payload }) { 2 | switch (type) { 3 | case 'REMOVE_FROM_BASKET': 4 | return { 5 | ...state, 6 | order: state.order.filter((el) => el.id !== payload.id), 7 | }; 8 | case 'CLOSE_ALERT': 9 | return { 10 | ...state, 11 | alertName: '', 12 | }; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /14-reducer-practice/finish/context.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer } from 'react'; 2 | import { reducer } from './reducer'; 3 | 4 | export const ShopContext = createContext(); 5 | 6 | const initialState = { 7 | goods: [], 8 | loading: true, 9 | order: [], 10 | isBasketShow: false, 11 | alertName: '', 12 | }; 13 | 14 | export const ContextProvider = ({ children }) => { 15 | const [value, dispatch] = useReducer(reducer, initialState); 16 | 17 | value.closeAlert = () => { 18 | dispatch({ type: 'CLOSE_ALERT' }); 19 | }; 20 | 21 | value.addToBasket = (item) => { 22 | dispatch({ type: 'ADD_TO_BASKET', payload: item }); 23 | }; 24 | 25 | value.incQuantity = (itemId) => { 26 | dispatch({ type: 'INCREMENT_QUANTITY', payload: { id: itemId } }); 27 | }; 28 | 29 | value.decQuantity = (itemId) => { 30 | dispatch({ type: 'DECREMENT_QUANTITY', payload: { id: itemId } }); 31 | }; 32 | 33 | value.removeFromBasket = (itemId) => { 34 | dispatch({ type: 'REMOVE_FROM_BASKET', payload: { id: itemId } }); 35 | }; 36 | 37 | value.handleBasketShow = () => { 38 | dispatch({ type: 'TOGGLE_BASKET' }); 39 | }; 40 | 41 | return ( 42 | {children} 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /14-reducer-practice/finish/reducer.js: -------------------------------------------------------------------------------- 1 | export function reducer(state, { type, payload }) { 2 | switch (type) { 3 | case 'ADD_TO_BASKET': { 4 | const itemIndex = state.order.findIndex( 5 | (orderItem) => orderItem.id === payload.id 6 | ); 7 | 8 | let newOrder = null; 9 | if (itemIndex < 0) { 10 | const newItem = { 11 | ...payload, 12 | quantity: 1, 13 | }; 14 | newOrder = [...state.order, newItem]; 15 | } else { 16 | newOrder = state.order.map((orderItem, index) => { 17 | if (index === itemIndex) { 18 | return { 19 | ...orderItem, 20 | quantity: orderItem.quantity + 1, 21 | }; 22 | } else { 23 | return orderItem; 24 | } 25 | }); 26 | } 27 | 28 | return { 29 | ...state, 30 | order: newOrder, 31 | alertName: payload.name, 32 | }; 33 | } 34 | case 'REMOVE_FROM_BASKET': 35 | return { 36 | ...state, 37 | order: state.order.filter((el) => el.id !== payload.id), 38 | }; 39 | case 'INCREMENT_QUANTITY': 40 | return { 41 | ...state, 42 | order: state.order.map((el) => { 43 | if (el.id === payload.id) { 44 | const newQuantity = el.quantity + 1; 45 | return { 46 | ...el, 47 | quantity: newQuantity, 48 | }; 49 | } else { 50 | return el; 51 | } 52 | }), 53 | }; 54 | case 'DECREMENT_QUANTITY': 55 | return { 56 | ...state, 57 | order: state.order.map((el) => { 58 | if (el.id === payload.id) { 59 | const newQuantity = el.quantity - 1; 60 | return { 61 | ...el, 62 | quantity: newQuantity >= 0 ? newQuantity : 0, 63 | }; 64 | } else { 65 | return el; 66 | } 67 | }), 68 | }; 69 | case 'CLOSE_ALERT': 70 | return { 71 | ...state, 72 | alertName: '', 73 | }; 74 | case 'TOGGLE_BASKET': 75 | return { 76 | ...state, 77 | isBasketShow: !state.isBasketShow, 78 | }; 79 | default: 80 | return state; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Практические задания к курсу "React с нуля для начинающих" 2 | 3 | Практические задания разбиты по директориям с соответствующей нумерацией. 4 | 5 | Внутри каждой практики есть две папки: 6 | - ***_start*** - содержит необходимые файлы для начала выполнения практического задания 7 | - ***finish*** - содержит один из возможных вариантов решения практического задания 8 | --------------------------------------------------------------------------------