├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── images
│ ├── iphone.png
│ ├── airpods.png
│ └── apple-watch.png
├── manifest.json
└── index.html
├── src
├── components
│ ├── ProductList.module.css
│ ├── ProductList.js
│ ├── ProductFilter.js
│ ├── ProductCard.module.css
│ └── ProductCard.js
├── App.module.css
├── setupTests.js
├── App.test.js
├── reportWebVitals.js
├── index.css
├── index.js
├── data
│ └── products.js
└── App.js
├── .gitignore
├── package.json
└── README.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/images/iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/images/iphone.png
--------------------------------------------------------------------------------
/public/images/airpods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/images/airpods.png
--------------------------------------------------------------------------------
/public/images/apple-watch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvasyliev/react-crash-course/HEAD/public/images/apple-watch.png
--------------------------------------------------------------------------------
/src/components/ProductList.module.css:
--------------------------------------------------------------------------------
1 | .List {
2 | display: flex;
3 | gap: 16px;
4 | margin-bottom: 36px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.module.css:
--------------------------------------------------------------------------------
1 | .App {
2 | max-width: 960px;
3 | margin: 0 auto;
4 | padding: 64px 16px 16px;
5 | }
6 |
7 | .ListDivider {
8 | border-color: slategray;
9 | }
10 |
11 | .ListTitle {
12 | margin: 8px 0;
13 | }
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/components/ProductList.js:
--------------------------------------------------------------------------------
1 | import styles from "./ProductList.module.css";
2 |
3 | export function ProductList(props) {
4 | return (
5 | <>
6 |
Products
7 | {props.children}
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | background-color: #282c34;
9 | min-height: 100vh;
10 | color: white;
11 | }
12 |
13 | code {
14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
15 | monospace;
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ProductFilter.js:
--------------------------------------------------------------------------------
1 | export function ProductFilter({ filters, onFilter }) {
2 | return (
3 |
4 | Price: $
5 | onFilter("min", event.target.value)}
11 | />{" "}
12 | - $
13 | onFilter("max", event.target.value)}
19 | />
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/ProductCard.module.css:
--------------------------------------------------------------------------------
1 | .Container {
2 | position: relative;
3 | width: 100%;
4 | border: 1px solid white;
5 | border-radius: 8px;
6 | padding: 16px;
7 | text-align: center;
8 | }
9 |
10 | .Favorite {
11 | position: absolute;
12 | top: 12px;
13 | right: 12px;
14 | border: 0;
15 | background: none;
16 | font-size: 20px;
17 |
18 | &:hover {
19 | top: 10px;
20 | right: 10px;
21 | font-size: 24px;
22 | }
23 | }
24 |
25 | .Specification {
26 | list-style: none;
27 | padding: 0;
28 | }
29 |
30 | .NotAvailableStatus {
31 | font-size: 14px;
32 | color: lightsalmon;
33 | }
34 |
35 | .AvailableStatus {
36 | font-size: 14px;
37 | color: lightgreen;
38 | }
39 |
--------------------------------------------------------------------------------
/src/data/products.js:
--------------------------------------------------------------------------------
1 | export const products = [
2 | {
3 | id: 1,
4 | imageSrc: "images/iphone.png",
5 | title: "iPhone 15 Pro",
6 | specification: [
7 | "A17 Pro chip with 6-core GPU",
8 | "3x or 5x Telephoto camera",
9 | "Up to 29 hours video playback",
10 | ],
11 | stockCount: 10,
12 | price: 999,
13 | },
14 | {
15 | id: 2,
16 | imageSrc: "images/airpods.png",
17 | title: "AirPods Pro 2",
18 | specification: [
19 | "Noise Cancellation",
20 | "Dust, sweat, and water resistant",
21 | "Up to 6 hours of listening",
22 | ],
23 | stockCount: 0,
24 | price: 249,
25 | },
26 | {
27 | id: 3,
28 | imageSrc: "images/apple-watch.png",
29 | title: "Apple Watch 9",
30 | specification: [
31 | "45mm or 41mm case size",
32 | "Always-On Retina display",
33 | "Up to 18 hours normal use",
34 | ],
35 | stockCount: 6,
36 | price: 399,
37 | },
38 | ];
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-crash-course",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "react": "^18.3.1",
10 | "react-dom": "^18.3.1",
11 | "react-scripts": "5.0.1",
12 | "web-vitals": "^2.1.4"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/ProductCard.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import styles from "./ProductCard.module.css";
3 |
4 | export function ProductCard({
5 | product,
6 | isFavorite,
7 | background = "slategray",
8 | onPurchase,
9 | onFavorite,
10 | }) {
11 | const [showMore, setShowMore] = useState(false);
12 |
13 | function handleClick() {
14 | onPurchase(product.id, product.stockCount - 1);
15 | }
16 |
17 | function handleTwoClicks() {
18 | onPurchase(product.id, product.stockCount - 2);
19 | }
20 |
21 | return (
22 |
23 | onFavorite(product.id)}
26 | >
27 | {isFavorite ? "❤️" : "🤍"}
28 |
29 | {product.title}
30 |
36 |
37 | Specification:{" "}
38 | setShowMore(!showMore)}>
39 | {showMore ? "hide" : "show"}
40 |
41 |
42 | {showMore && (
43 |
44 | {product.specification.map((spec, index) => (
45 | {spec}
46 | ))}
47 |
48 | )}
49 |
50 | {product.stockCount > 0 && (
51 | <>
52 | Price: ${product.price}
53 | Buy
54 | >
55 | )}
56 | {product.stockCount > 1 && (
57 | Buy 2
58 | )}
59 |
60 | );
61 | }
62 |
63 | function Status({ stockCount }) {
64 | const notAvailableTemplate = (
65 | Not available
66 | );
67 |
68 | const availableTemplate = (
69 | {stockCount} items available
70 | );
71 |
72 | return stockCount === 0 ? notAvailableTemplate : availableTemplate;
73 | }
74 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Fragment } from "react";
3 | import { ProductList } from "./components/ProductList";
4 | import { ProductCard } from "./components/ProductCard";
5 | import { ProductFilter } from "./components/ProductFilter";
6 | import { products as productsData } from "./data/products";
7 | import styles from "./App.module.css";
8 |
9 | function App() {
10 | const [products, setProducts] = useState(productsData);
11 | const [filters, setFilters] = useState({
12 | price: {
13 | min: 0,
14 | max: 999,
15 | },
16 | other: "other value",
17 | });
18 | const [favorites, setFavorites] = useState([]);
19 |
20 | function handlePurchase(productId, stockCount) {
21 | setProducts((prevProducts) =>
22 | prevProducts.map((product) =>
23 | product.id === productId ? { ...product, stockCount } : product
24 | )
25 | );
26 | }
27 |
28 | function handleFilter(key, value) {
29 | setFilters((prevFilters) => ({
30 | ...prevFilters,
31 | price: {
32 | ...prevFilters.price,
33 | [key]: value,
34 | },
35 | }));
36 | }
37 |
38 | function handleFavorite(productId) {
39 | if (favorites.includes(productId)) {
40 | setFavorites((prevFavotites) =>
41 | prevFavotites.filter((id) => id !== productId)
42 | );
43 | } else {
44 | setFavorites((prevFavotites) => [...prevFavotites, productId]);
45 | }
46 | }
47 |
48 | return (
49 |
50 |
51 | {products.map((product) => (
52 |
59 | ))}
60 |
61 |
62 |
Products filtered by price
63 |
64 | {products
65 | .filter(
66 | ({ price }) =>
67 | price >= filters.price.min && price <= filters.price.max
68 | )
69 | .map(({ title, price }) => (
70 |
71 |
72 |
73 | {title} cost ${price}
74 |
75 |
76 | ))}
77 |
78 | );
79 | }
80 |
81 | export default App;
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | About me ◽️ 🎬 Udemy Instructor ◽️ 👨🏻💻 Front-End Engineer (10+ years) ◽️ 🎯 Mentor for Developers
2 |
3 | # Products List App
4 |
5 | This repository contains the Source Code for the "Products List App" from 🧑🏻💻 [React Crash Course: From Zero to Hero](https://linkly.link/1zdiJ) course.
6 |
7 | # My Other Courses
8 |
9 | #### Front-End Courses
10 |
11 | 🧑🏻💻 [React Crash Course: From Zero to Hero](https://linkly.link/1zdiJ)\
12 | 🤖 [React.js AI Chatbot App with ChatGPT, Gemini AI and DeepSeek](https://linkly.link/2DUBq)
13 |
14 | #### AI & Vibe Coding Courses
15 |
16 | 😈 [GitHub Copilot for Beginners: AI Coding Crash Course](https://linkly.link/2B4WN)\
17 | 👻 [Cursor AI for Beginners: AI Coding Crash Course](https://linkly.link/2CKBT)\
18 | 🏄🏻♂️ [Windsurf for Beginners: AI Coding Crash Course ](https://linkly.link/2DUC9)\
19 | ❤️ [Lovable AI: Complete Guide for Vibe Coding](https://linkly.link/2DUC1)
20 |
21 | #### Full-Stack Courses
22 |
23 | 👓 [Next.js Crash Course: Build a Full-Stack App in a Weekend](https://linkly.link/2EUQ5)\
24 | 🧩 [Node.js Crash Course: Build a REST API in a Weekend](https://linkly.link/2EUQ6)
25 |
26 | # How to use Code Examples from Github
27 |
28 | ## Download the code source
29 |
30 | ### a) Download code source for final application
31 |
32 | 1. Open repository main [page](https://github.com/dvasyliev/react-crash-course).
33 |
34 | 2. Click on the green "Code" button to open a list and then click on the "Download ZIP" button to download a source code.
35 |
36 |
37 |
38 | ### b) Download code sources for specific lesson
39 |
40 | 1. Open [commits](https://github.com/dvasyliev/react-crash-course/commits/main/) page.
41 |
42 | 2. Click on the "code" icon near the lesson you are interested.
43 |
44 |
45 |
46 | 3. You will be redirected to the specific source code page for this lesson.
47 |
48 | 4. Click on the green "Code" button to open a list and then click on the "Download ZIP" button to download a source code.
49 |
50 |
51 |
52 | ## Open project in the VS code (code editor)
53 |
54 | Open the archive on your computer, open VS code (code editor) and cick "File" -> "Open Folder" -> Choose project folder => "Open".
55 |
56 |
57 |
58 | ## Start application
59 |
60 | ### 1. Install dependencies
61 |
62 | Run the `npm install` command to install all the libraries needed to run the application.
63 |
64 | ### 2. Run application
65 |
66 | Run the `npm start` command in terminal to start the application.
67 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
68 | The page will reload when you make changes.
69 |
--------------------------------------------------------------------------------