├── .babelrc
├── .gitignore
├── .prettierrc.json
├── .vscode
└── launch.json
├── README.md
├── components
├── cart-item.js
├── cart-item.unit.spec.js
├── cart.integration.spec.js
├── cart.js
├── product-card.js
├── product-card.unit.spec.js
├── search.js
└── search.unit.spec.js
├── hooks
├── use-fetch-products.js
└── use-fetch-products.unit.spec.js
├── jest-integration.config.js
├── jest-unit.config.js
├── jest.config.js
├── miragejs
├── .gitignore
├── .prettierrc.json
├── README.md
├── factories
│ ├── index.js
│ ├── message.js
│ ├── product.js
│ ├── user.js
│ └── utils.js
├── models
│ └── index.js
├── routes
│ └── index.js
├── seeds
│ └── index.js
└── server.js
├── package.json
├── pages-tests
└── product-list.integration.spec.js
├── pages
├── _app.js
└── index.js
├── postcss.config.js
├── public
└── favicon.ico
├── setupTests.js
├── store
└── cart
│ ├── cart-store.unit.spec.js
│ └── index.js
├── styles
└── globals.css
├── tailwind.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 | *.code-workspace
36 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 80
5 | }
6 |
--------------------------------------------------------------------------------
/.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": "node",
9 | "name": "vscode-jest-tests",
10 | "request": "launch",
11 | "args": [
12 | "--runInBand"
13 | ],
14 | "cwd": "${workspaceFolder}",
15 | "console": "integratedTerminal",
16 | "internalConsoleOptions": "neverOpen",
17 | "disableOptimisticBPs": true,
18 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following resources:
20 |
21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23 |
24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25 |
26 | ## Deploy on Vercel
27 |
28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
29 |
30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
31 |
--------------------------------------------------------------------------------
/components/cart-item.js:
--------------------------------------------------------------------------------
1 | import { useCartStore } from '../store/cart';
2 |
3 | export default function CartItem({ product }) {
4 | const { remove, increase, decrease } = useCartStore((store) => store.actions);
5 |
6 | return (
7 |
8 |
9 |

15 |
16 |
{product.title}
17 |
24 |
25 |
42 |
43 | {product.quantity}
44 |
45 |
62 |
63 |
64 |
65 |
${product.price}
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/cart-item.unit.spec.js:
--------------------------------------------------------------------------------
1 | import { screen, render, fireEvent } from '@testing-library/react';
2 | import CartItem from './cart-item';
3 | import userEvent from '@testing-library/user-event';
4 | import { renderHook } from '@testing-library/react-hooks';
5 | import { useCartStore } from '../store/cart';
6 | import { setAutoFreeze } from 'immer';
7 |
8 | setAutoFreeze(false);
9 |
10 | const product = {
11 | title: 'Relógio bonito',
12 | price: '22.00',
13 | image:
14 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
15 | };
16 |
17 | const renderCartItem = () => {
18 | render();
19 | };
20 |
21 | describe('CartItem', () => {
22 | let result;
23 |
24 | beforeEach(() => {
25 | result = renderHook(() => useCartStore()).result;
26 | });
27 |
28 | it('should render CartItem', () => {
29 | renderCartItem();
30 |
31 | expect(screen.getByTestId('cart-item')).toBeInTheDocument();
32 | });
33 |
34 | it('should display proper content', () => {
35 | renderCartItem();
36 |
37 | const image = screen.getByTestId('image');
38 |
39 | expect(
40 | screen.getByText(new RegExp(product.title, 'i')),
41 | ).toBeInTheDocument();
42 | expect(
43 | screen.getByText(new RegExp(product.price, 'i')),
44 | ).toBeInTheDocument();
45 | expect(image).toHaveProperty('src', product.image);
46 | expect(image).toHaveProperty('alt', product.title);
47 | });
48 |
49 | it('should call remove() when remove button is clicked', async () => {
50 | const spy = jest.spyOn(result.current.actions, 'remove');
51 |
52 | renderCartItem();
53 |
54 | const button = screen.getByRole('button', { name: /remove/i });
55 |
56 | await userEvent.click(button);
57 |
58 | expect(spy).toHaveBeenCalledTimes(1);
59 | expect(spy).toHaveBeenCalledWith(product);
60 | });
61 |
62 | it('should call increase() when increase button is clicked', async () => {
63 | const spy = jest.spyOn(result.current.actions, 'increase');
64 |
65 | renderCartItem();
66 |
67 | const button = screen.getByTestId('increase');
68 |
69 | await userEvent.click(button);
70 |
71 | expect(spy).toHaveBeenCalledTimes(1);
72 | expect(spy).toHaveBeenCalledWith(product);
73 | });
74 |
75 | it('should call increase() when increase button is clicked', async () => {
76 | const spy = jest.spyOn(result.current.actions, 'decrease');
77 |
78 | renderCartItem();
79 |
80 | const button = screen.getByTestId('decrease');
81 |
82 | await userEvent.click(button);
83 |
84 | expect(spy).toHaveBeenCalledTimes(1);
85 | expect(spy).toHaveBeenCalledWith(product);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/components/cart.integration.spec.js:
--------------------------------------------------------------------------------
1 | import { renderHook, act as hooksAct } from '@testing-library/react-hooks';
2 | import { screen, render } from '@testing-library/react';
3 | import { useCartStore } from '../store/cart';
4 | import { makeServer } from '../miragejs/server';
5 | import { setAutoFreeze } from 'immer';
6 | import userEvent from '@testing-library/user-event';
7 | import Cart from './cart';
8 | import TestRenderer from 'react-test-renderer';
9 |
10 | const { act: componentsAct } = TestRenderer;
11 |
12 | setAutoFreeze(false);
13 |
14 | describe('Cart', () => {
15 | let server;
16 | let result;
17 | let spy;
18 | let add;
19 | let toggle;
20 | let reset;
21 |
22 | beforeEach(() => {
23 | server = makeServer({ environment: 'test' });
24 | result = renderHook(() => useCartStore()).result;
25 | add = result.current.actions.add;
26 | reset = result.current.actions.reset;
27 | toggle = result.current.actions.toggle;
28 | spy = jest.spyOn(result.current.actions, 'toggle');
29 | });
30 |
31 | afterEach(() => {
32 | server.shutdown();
33 | jest.clearAllMocks();
34 | });
35 |
36 | it('should add css class "hidden" in the component', () => {
37 | render();
38 |
39 | expect(screen.getByTestId('cart')).toHaveClass('hidden');
40 | });
41 |
42 | it('should remove css class "hidden" in the component', async () => {
43 | await componentsAct(async () => {
44 | render();
45 |
46 | await userEvent.click(screen.getByTestId('close-button'));
47 |
48 | expect(screen.getByTestId('cart')).not.toHaveClass('hidden');
49 | });
50 | });
51 |
52 | it('should call store toggle() twice', async () => {
53 | await componentsAct(async () => {
54 | render();
55 |
56 | const button = screen.getByTestId('close-button');
57 |
58 | await userEvent.click(button);
59 | await userEvent.click(button);
60 |
61 | expect(spy).toHaveBeenCalledTimes(2);
62 | });
63 | });
64 |
65 | it('should display 2 products cards', () => {
66 | const products = server.createList('product', 2);
67 |
68 | hooksAct(() => {
69 | for (const product of products) {
70 | add(product);
71 | }
72 | });
73 |
74 | render();
75 |
76 | expect(screen.getAllByTestId('cart-item')).toHaveLength(2);
77 | });
78 |
79 | it('should remove all products when clear cart button is clicked', async () => {
80 | const products = server.createList('product', 2);
81 |
82 | hooksAct(() => {
83 | for (const product of products) {
84 | add(product);
85 | }
86 | });
87 |
88 | await componentsAct(async () => {
89 | render();
90 |
91 | expect(screen.getAllByTestId('cart-item')).toHaveLength(2);
92 |
93 | const button = screen.getByRole('button', { name: /clear cart/i });
94 |
95 | await userEvent.click(button);
96 |
97 | expect(screen.queryAllByTestId('cart-item')).toHaveLength(0);
98 | });
99 | });
100 |
101 | it('should not display clear cart button if no products are in the cart', async () => {
102 | render();
103 |
104 | expect(
105 | screen.queryByRole('button', { name: /clear cart/i }),
106 | ).not.toBeInTheDocument();
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/components/cart.js:
--------------------------------------------------------------------------------
1 | import CartItem from './cart-item';
2 | import { useCartStore } from '../store/cart';
3 |
4 | export default function Cart() {
5 | const { open, products } = useCartStore((store) => store.state);
6 | const { toggle, removeAll } = useCartStore((store) => store.actions);
7 |
8 | const hasProducts = products.length > 0;
9 |
10 | return (
11 |
17 |
18 |
Your cart
19 | {hasProducts ? (
20 |
27 | ) : null}
28 |
45 |
46 |
47 | {!hasProducts ? (
48 |
49 | There are no items in the cart
50 |
51 | ) : null}
52 | {products.map((product) => (
53 |
54 | ))}
55 | {hasProducts ? (
56 |
57 | Checkout
58 |
69 |
70 | ) : null}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/components/product-card.js:
--------------------------------------------------------------------------------
1 | export default function ProductCard({ product, addToCart }) {
2 | return (
3 |
4 |
5 |
12 |
28 |
29 |
30 |
{product.title}
31 | ${product.price}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/product-card.unit.spec.js:
--------------------------------------------------------------------------------
1 | import { screen, render, fireEvent } from '@testing-library/react';
2 | import ProductCard from './product-card';
3 |
4 | const product = {
5 | title: 'Relógio bonito',
6 | price: '22.00',
7 | image:
8 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
9 | };
10 |
11 | const addToCart = jest.fn();
12 |
13 | const renderProductCard = () => {
14 | render();
15 | };
16 |
17 | describe('ProductCard', () => {
18 | it('should render ProductCard', () => {
19 | renderProductCard();
20 |
21 | expect(screen.getByTestId('product-card')).toBeInTheDocument();
22 | });
23 |
24 | it('should display proper content', () => {
25 | renderProductCard();
26 |
27 | expect(screen.getByText(new RegExp(product.title, 'i'))).toBeInTheDocument();
28 | expect(screen.getByText(new RegExp(product.price, 'i'))).toBeInTheDocument();
29 | expect(screen.getByTestId('image')).toHaveStyle({
30 | backgroundImage: product.image,
31 | });
32 | });
33 |
34 | it('should call props.addToCart() when button gets clicked', async () => {
35 | renderProductCard();
36 |
37 | const button = screen.getByRole('button');
38 |
39 | await fireEvent.click(button);
40 |
41 | expect(addToCart).toHaveBeenCalledTimes(1);
42 | expect(addToCart).toHaveBeenCalledWith(product);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/components/search.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export default function Search({ doSearch }) {
4 | const [term, setTerm] = useState('');
5 |
6 | const submitHandler = (ev) => {
7 | ev.preventDefault();
8 | doSearch(term);
9 | };
10 |
11 | const inputHandler = (ev) => {
12 | setTerm(ev.target.value);
13 |
14 | if (ev.target.value === '') {
15 | doSearch('');
16 | }
17 | };
18 |
19 | return (
20 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/search.unit.spec.js:
--------------------------------------------------------------------------------
1 | import Search from './search';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | const doSearch = jest.fn();
6 |
7 | describe('Search', () => {
8 | afterEach(() => {
9 | jest.clearAllMocks();
10 | });
11 |
12 | it('should render a form', () => {
13 | render();
14 |
15 | expect(screen.getByRole('form')).toBeInTheDocument();
16 | });
17 |
18 | it('should render a input type equals search', () => {
19 | render();
20 |
21 | expect(screen.getByRole('searchbox')).toHaveProperty('type', 'search');
22 | });
23 |
24 | it('should call props.doSearch() when form is submitted', async () => {
25 | render();
26 |
27 | const form = screen.getByRole('form');
28 |
29 | await fireEvent.submit(form);
30 |
31 | expect(doSearch).toHaveBeenCalledTimes(1);
32 | });
33 |
34 | it('should call props.doSearch() with the user input', async () => {
35 | render();
36 |
37 | const inputText = 'some text here';
38 | const form = screen.getByRole('form');
39 | const input = screen.getByRole('searchbox');
40 |
41 | await userEvent.type(input, inputText);
42 | await fireEvent.submit(form);
43 |
44 | expect(doSearch).toHaveBeenCalledWith(inputText);
45 | });
46 |
47 | it('should call doSearch when search input is cleared', async () => {
48 | render();
49 |
50 | const inputText = 'some text here';
51 | const input = screen.getByRole('searchbox');
52 |
53 | await userEvent.type(input, inputText);
54 | await userEvent.clear(input);
55 |
56 | expect(doSearch).toHaveBeenCalledTimes(1);
57 | expect(doSearch).toHaveBeenCalledWith('');
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/hooks/use-fetch-products.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | export const useFetchProducts = () => {
5 | const [products, setProducts] = useState([]);
6 | const [error, setError] = useState(false);
7 |
8 | useEffect(() => {
9 | let mounted = true;
10 | axios
11 | .get('/api/products')
12 | .then((res) => {
13 | if (mounted) {
14 | setProducts(res.data.products);
15 | }
16 | })
17 | .catch((error) => {
18 | if (mounted) {
19 | setError(true);
20 | }
21 | });
22 |
23 | return () => (mounted = false);
24 | }, []);
25 |
26 | return { products, error };
27 | };
28 |
--------------------------------------------------------------------------------
/hooks/use-fetch-products.unit.spec.js:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks';
2 | import { useFetchProducts } from './use-fetch-products';
3 | import { makeServer } from '../miragejs/server';
4 | import Response from 'miragejs';
5 |
6 | describe('useFetchProducts', () => {
7 | let server;
8 |
9 | beforeEach(() => {
10 | server = makeServer({ environment: 'test' });
11 | });
12 |
13 | afterEach(() => {
14 | server.shutdown();
15 | });
16 |
17 | it('should return a list of 10 products', async () => {
18 | server.createList('product', 10);
19 |
20 | const { result, waitForNextUpdate } = renderHook(() => useFetchProducts());
21 |
22 | await waitForNextUpdate();
23 |
24 | expect(result.current.products).toHaveLength(10);
25 | expect(result.current.error).toBe(false);
26 | });
27 |
28 | it('should set error to true when catch() block is executed', async () => {
29 | server.get('products', () => {
30 | return new Response(500, {}, '');
31 | });
32 |
33 | const { result, waitForNextUpdate } = renderHook(() => useFetchProducts());
34 |
35 | await waitForNextUpdate();
36 |
37 | expect(result.current.error).toBe(true);
38 | expect(result.current.products).toHaveLength(0);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/jest-integration.config.js:
--------------------------------------------------------------------------------
1 | const config = require('./jest.config');
2 |
3 | module.exports = {
4 | ...config,
5 | testMatch: ['**/?(*.integration.)+(spec|test).[jt]s?(x)'],
6 | };
7 |
--------------------------------------------------------------------------------
/jest-unit.config.js:
--------------------------------------------------------------------------------
1 | const config = require('./jest.config');
2 |
3 | module.exports = {
4 | ...config,
5 | testMatch: ['**/?(*.unit.)+(spec|test).[jt]s?(x)'],
6 | };
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "jsdom",
3 | testPathIgnorePatterns: ['/.next/', '/node_modules/'],
4 | setupFilesAfterEnv: ['/setupTests.js'],
5 | transform: {
6 | '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest',
7 | },
8 | collectCoverageFrom: [
9 | '/components/**/*.js',
10 | '/pages/**/*.js',
11 | '/hooks/**/*.js',
12 | '/store/**/*.js',
13 | ],
14 | watchPlugins: [
15 | 'jest-watch-typeahead/filename',
16 | 'jest-watch-typeahead/testname',
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/miragejs/.gitignore:
--------------------------------------------------------------------------------
1 | TODO.txt
2 |
--------------------------------------------------------------------------------
/miragejs/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 100
5 | }
6 |
--------------------------------------------------------------------------------
/miragejs/README.md:
--------------------------------------------------------------------------------
1 | # Mirage JS Starter Kit
2 |
3 | [From Mirage website](https://miragejs.com/):
4 |
5 | > Mirage JS is an API mocking library that lets you build, test and share a complete working JavaScript application without having to rely on any backend services.
6 |
7 | Their documentation is straight to the point but it only helps you setting up a simple server without any suggestion of folder organization. This starter kit keeps things separate into their own folders, introducing separation of concerns and making it easier to reason about all the parts.
8 |
9 | ---
10 |
11 | ## How to use it
12 |
13 | ### 1. Having started a React or Vue.js project, copy all the files to the src folder:
14 |
15 | ```
16 | cd src && npx degit vedovelli/miragejs-starter-kit miragejs
17 | ```
18 |
19 | [What is **degit**?](https://github.com/Rich-Harris/degit#readme)
20 |
21 | This will create the `miragejs` folder inside `src`. You can use any folder name you find best.
22 |
23 | **IMPORTANT**: do NOT omit the folder name in the degit command otherwise all starter kit's folders will be created inside your src folder, messing up your project organization.
24 |
25 | ### 2. Make sure all dependencies are installed:
26 |
27 | ```
28 | npm install --save-dev miragejs faker
29 | ```
30 |
31 | ### 3. Make your project aware of Mirage JS:
32 |
33 | **React**
34 |
35 | ```
36 | import React from "react";
37 | import ReactDOM from "react-dom";
38 | import App from "./App";
39 |
40 | if (process.env.NODE_ENV === "development") {
41 | // You can't use import in a conditional so we're using require() so no
42 | // Mirage JS code will ever reach your production build.
43 | require('./miragejs/server').makeServer();
44 | }
45 |
46 | ReactDOM.render(, document.getElementById("root"));
47 | ```
48 |
49 | **Vue.js**
50 |
51 | ```
52 | import Vue from "vue";
53 | import App from "./App.vue";
54 | import router from "./router";
55 | import store from "./store";
56 |
57 | if (process.env.NODE_ENV === "development") {
58 | // You can't use import in a conditional so we're using require() so no
59 | // Mirage JS code will ever reach your production build.
60 | require('./miragejs/server').makeServer();
61 | }
62 |
63 | Vue.config.productionTip = false;
64 |
65 | new Vue({
66 | router,
67 | store,
68 | render: h => h(App)
69 | }).$mount("#app");
70 | ```
71 |
72 | ### 4. Calling the API
73 |
74 | Inside any component of your application and using your favorite HTTP request's library make a call to `api/users`. You will receive back a list of 10 objects with the following shape:
75 |
76 | ```
77 | {id: "1", name: "Some name", mobile: "+1 555 525636"}
78 | ```
79 |
80 | Additionally if you call `api/products` you'll receive back a list of 3 objects with the following shape:
81 |
82 | ```
83 | {
84 | id: "1",
85 | name: "Javascript coffee mug",
86 | description: "We are nothing without coffee",
87 | price: 3.5
88 | },
89 | ```
90 |
91 | Those routes operate with a `resource` meaning they accept all HTTP verbs involved in a CRUD operation.
92 |
93 | To get all the messages associated with an user make a call to `api/messages?userId=`.
94 |
95 | ### 5. Adding your own content
96 |
97 | Lastly tweak `factories`, `fixtures` and `seeds` to accommodate your own needs.
98 |
99 | ---
100 |
101 | ## Folder structure
102 |
103 | - `factories`: contains the blueprints for the content to be generated by mirage. It uses [faker](https://github.com/Marak/Faker.js#readme) to generate random but credible content;
104 | - `fixtures`: contains predefined data to be served by Mirage. Use it if you have content to be served from JSON files;
105 | - `models`: contains all models for the database entities. Every time you create a new resource or a new fixture, it is necessary to create a new model;
106 | - `routes`: contains the routes for your API;
107 | - `seeds`: contains the seeds for the data. They determine how many records should be generated and stored in the database. For that purpose they use the Factories.
108 |
109 | ## Example projects
110 |
111 | To help you see it in action there are 2 example projects. For each one of them all instructions above were followed and the data is displayed in the interface.
112 |
113 | **React**: [https://github.com/vedovelli/miragejs-starter-kit-react-example](https://github.com/vedovelli/miragejs-starter-kit-react-example)
114 |
115 | **Vue.js**: [https://github.com/vedovelli/miragejs-starter-kit-vuejs-example](https://github.com/vedovelli/miragejs-starter-kit-vuejs-example)
116 |
--------------------------------------------------------------------------------
/miragejs/factories/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories
3 | */
4 |
5 | import user from './user';
6 | import message from './message';
7 | import product from './product';
8 |
9 | /*
10 | * factories are contained in a single object, that's why we
11 | * destructure what's coming from users and the same should
12 | * be done for all future factories
13 | */
14 | export default {
15 | ...user,
16 | ...message,
17 | ...product,
18 | };
19 |
--------------------------------------------------------------------------------
/miragejs/factories/message.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories
3 | */
4 | import { Factory } from 'miragejs';
5 |
6 | /*
7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme
8 | */
9 | import faker from 'faker';
10 |
11 | export default {
12 | message: Factory.extend({
13 | content() {
14 | return faker.fake('{{lorem.paragraph}}');
15 | },
16 | date() {
17 | const date = new Date(faker.fake('{{date.past}}'));
18 | return date.toLocaleDateString();
19 | },
20 | }),
21 | };
22 |
--------------------------------------------------------------------------------
/miragejs/factories/product.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories
3 | */
4 | import { Factory } from 'miragejs';
5 |
6 | /*
7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme
8 | */
9 | import faker from 'faker';
10 |
11 | import { randomNumber } from './utils';
12 |
13 | const images = [
14 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
15 | 'https://images.unsplash.com/photo-1524592094714-0f0654e20314?ixlib=rb-1.2.1&auto=format&fit=crop&w=689&q=80',
16 | 'https://images.unsplash.com/photo-1532667449560-72a95c8d381b?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
17 | 'https://images.unsplash.com/photo-1542496658-e33a6d0d50f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
18 | 'https://images.unsplash.com/photo-1434056886845-dac89ffe9b56?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
19 | 'https://images.unsplash.com/photo-1526045431048-f857369baa09?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
20 | 'https://images.unsplash.com/photo-1495857000853-fe46c8aefc30?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
21 | 'https://images.unsplash.com/photo-1444881421460-d838c3b98f95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80',
22 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
23 | 'https://images.unsplash.com/photo-1524592094714-0f0654e20314?ixlib=rb-1.2.1&auto=format&fit=crop&w=689&q=80',
24 | 'https://images.unsplash.com/photo-1532667449560-72a95c8d381b?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
25 | 'https://images.unsplash.com/photo-1542496658-e33a6d0d50f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
26 | 'https://images.unsplash.com/photo-1434056886845-dac89ffe9b56?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
27 | 'https://images.unsplash.com/photo-1526045431048-f857369baa09?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
28 | 'https://images.unsplash.com/photo-1495857000853-fe46c8aefc30?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
29 | 'https://images.unsplash.com/photo-1444881421460-d838c3b98f95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80',
30 | ];
31 |
32 | export default {
33 | product: Factory.extend({
34 | title() {
35 | return faker.fake('{{lorem.words}}');
36 | },
37 | price() {
38 | return faker.fake('{{commerce.price}}');
39 | },
40 | image() {
41 | return images[randomNumber(10)];
42 | },
43 | }),
44 | };
45 |
--------------------------------------------------------------------------------
/miragejs/factories/user.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories
3 | */
4 | import { Factory } from 'miragejs';
5 |
6 | /*
7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme
8 | */
9 | import faker from 'faker';
10 | import { randomNumber } from './utils';
11 |
12 | export default {
13 | user: Factory.extend({
14 | name() {
15 | return faker.fake('{{name.findName}}');
16 | },
17 | mobile() {
18 | return faker.fake('{{phone.phoneNumber}}');
19 | },
20 | afterCreate(user, server) {
21 | const messages = server.createList('message', randomNumber(10), { user });
22 |
23 | user.update({ messages });
24 | },
25 | }),
26 | };
27 |
--------------------------------------------------------------------------------
/miragejs/factories/utils.js:
--------------------------------------------------------------------------------
1 | export const randomNumber = (quantity) => Math.floor(Math.random() * quantity) + 1;
2 |
--------------------------------------------------------------------------------
/miragejs/models/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Models: https://miragejs.com/docs/data-layer/models
3 | */
4 |
5 | import { Model, hasMany, belongsTo } from 'miragejs';
6 |
7 | /*
8 | * Everytime you create a new resource you have
9 | * to create a new Model and add it here. It is
10 | * true for Factories and for Fixtures.
11 | *
12 | * Mirage JS guide on Relationships: https://miragejs.com/docs/main-concepts/relationships/
13 | */
14 | export default {
15 | user: Model.extend({
16 | messages: hasMany(),
17 | }),
18 | messages: Model.extend({
19 | user: belongsTo(),
20 | }),
21 | product: Model,
22 | };
23 |
--------------------------------------------------------------------------------
/miragejs/routes/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Routes: https://miragejs.com/docs/route-handlers/functions
3 | */
4 |
5 | // import { Response } from 'miragejs';
6 |
7 | export default function routes() {
8 | this.namespace = 'api';
9 | // this.timing = 5000;
10 |
11 | /*
12 | * A resource comprises all operations for a CRUD
13 | * operation. .get(), .post(), .put() and delete().
14 | * Mirage JS guide on Resource: https://miragejs.com/docs/route-handlers/shorthands#resource-helper
15 | */
16 | this.resource('users');
17 | this.resource('products');
18 | // this.get('products', () => {
19 | // return new Response(500, {}, 'O server morreu!');
20 | // });
21 |
22 | /*
23 | * From your component use fetch('api/messages?userId=')
24 | * replacing with a real ID
25 | */
26 | this.get('messages', (schema, request) => {
27 | const {
28 | queryParams: { userId },
29 | } = request;
30 |
31 | return schema.messages.where({ userId });
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/miragejs/seeds/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mirage JS guide on Seeds: https://miragejs.com/docs/data-layer/factories#in-development
3 | */
4 |
5 | const usersSeeder = (server) => {
6 | /*
7 | * This will create in the in memory DB 10 objects
8 | * of the Factory `user`. Moreover it creates a
9 | * random number of messages and assign to each
10 | * and every user, making use of relationships.
11 | */
12 | server.createList('user', 10);
13 | };
14 |
15 | const productsSeeder = (server) => {
16 | server.createList('product', 25);
17 | };
18 |
19 | export default function seeds(server) {
20 | usersSeeder(server);
21 | productsSeeder(server);
22 | }
23 |
--------------------------------------------------------------------------------
/miragejs/server.js:
--------------------------------------------------------------------------------
1 | import { Server } from 'miragejs';
2 | import factories from './factories';
3 | import routes from './routes';
4 | import models from './models';
5 | import seeds from './seeds';
6 |
7 | const config = (environment) => {
8 | const config = {
9 | environment,
10 | factories,
11 | models,
12 | routes,
13 | seeds,
14 | };
15 |
16 | return config;
17 | };
18 |
19 | export function makeServer({ environment = 'development' } = {}) {
20 | return new Server(config(environment));
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "curso-javascript-testes-modulo-3",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "test": "jest",
8 | "test:unit": "jest --config=jest-unit.config.js",
9 | "test:unit:watch": "jest --config=jest-unit.config.js --watchAll",
10 | "test:integration": "jest --config=jest-integration.config.js",
11 | "test:integration:watch": "jest --config=jest-integration.config.js --watchAll",
12 | "test:watch": "jest --watchAll",
13 | "test:coverage": "jest --coverage",
14 | "build": "next build",
15 | "start": "next start"
16 | },
17 | "dependencies": {
18 | "@tailwindcss/ui": "0.7.2",
19 | "axios": "0.25.0",
20 | "immer": "9.0.12",
21 | "next": "12.0.8",
22 | "react": "17.0.2",
23 | "react-dom": "17.0.2",
24 | "tailwindcss": "3.0.15",
25 | "zustand": "3.6.9"
26 | },
27 | "devDependencies": {
28 | "@testing-library/dom": "8.11.2",
29 | "@testing-library/jest-dom": "5.16.1",
30 | "@testing-library/react": "12.1.2",
31 | "@testing-library/react-hooks": "7.0.2",
32 | "@testing-library/user-event": "13.5.0",
33 | "babel-jest": "27.4.6",
34 | "faker": "5.5.3",
35 | "jest": "27.4.7",
36 | "jest-dom": "^4.0.0",
37 | "jest-watch-typeahead": "1.0.0",
38 | "miragejs": "0.1.43",
39 | "postcss": "^8.4.5",
40 | "postcss-flexbugs-fixes": "5.0.2",
41 | "postcss-preset-env": "^7.2.3",
42 | "react-test-renderer": "17.0.2"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages-tests/product-list.integration.spec.js:
--------------------------------------------------------------------------------
1 | import { screen, render, waitFor, fireEvent } from '@testing-library/react';
2 | import ProductList from '../pages';
3 | import { makeServer } from '../miragejs/server';
4 | import Response from 'miragejs';
5 | import userEvent from '@testing-library/user-event';
6 |
7 | const renderProductList = () => {
8 | render();
9 | };
10 |
11 | describe('ProductList', () => {
12 | let server;
13 |
14 | beforeEach(() => {
15 | server = makeServer({ environment: 'test' });
16 | });
17 |
18 | afterEach(() => {
19 | server.shutdown();
20 | });
21 |
22 | it('should render ProductList', () => {
23 | renderProductList();
24 |
25 | expect(screen.getByTestId('product-list')).toBeInTheDocument();
26 | });
27 |
28 | it('should render the ProductCard component 10 times', async () => {
29 | server.createList('product', 10);
30 |
31 | renderProductList();
32 |
33 | await waitFor(() => {
34 | expect(screen.getAllByTestId('product-card')).toHaveLength(10);
35 | });
36 | });
37 |
38 | it('should render the "no products message"', async () => {
39 | renderProductList();
40 |
41 | await waitFor(() => {
42 | expect(screen.getByTestId('no-products')).toBeInTheDocument();
43 | });
44 | });
45 |
46 | it('should display error message when promise rejects', async () => {
47 | server.get('products', () => {
48 | return new Response(500, {}, '');
49 | });
50 |
51 | renderProductList();
52 |
53 | await waitFor(() => {
54 | expect(screen.getByTestId('server-error')).toBeInTheDocument();
55 | expect(screen.queryByTestId('no-products')).toBeNull();
56 | expect(screen.queryAllByTestId('product-card')).toHaveLength(0);
57 | });
58 | });
59 |
60 | it('should filter the product list when a search is performed', async () => {
61 | const searchTerm = 'Relógio bonito';
62 |
63 | server.createList('product', 2);
64 |
65 | server.create('product', {
66 | title: searchTerm,
67 | });
68 |
69 | renderProductList();
70 |
71 | await waitFor(() => {
72 | expect(screen.getAllByTestId('product-card')).toHaveLength(3);
73 | });
74 |
75 | const form = screen.getByRole('form');
76 | const input = screen.getByRole('searchbox');
77 |
78 | await userEvent.type(input, searchTerm);
79 | await fireEvent.submit(form);
80 |
81 | await waitFor(() => {
82 | expect(screen.getAllByTestId('product-card')).toHaveLength(1);
83 | });
84 | });
85 |
86 | it('should display the total quantity of products', async () => {
87 | server.createList('product', 10);
88 |
89 | renderProductList();
90 |
91 | await waitFor(() => {
92 | expect(screen.getByText(/10 Products/i)).toBeInTheDocument();
93 | });
94 | });
95 |
96 | it('should display product (singular) when there is only 1 product', async () => {
97 | server.create('product');
98 |
99 | renderProductList();
100 |
101 | await waitFor(() => {
102 | expect(screen.getByText(/1 Product$/i)).toBeInTheDocument();
103 | });
104 | });
105 |
106 | it('should display proper quantity when list is filtered', async () => {
107 | const searchTerm = 'Relógio bonito';
108 |
109 | server.createList('product', 2);
110 |
111 | server.create('product', {
112 | title: searchTerm,
113 | });
114 |
115 | renderProductList();
116 |
117 | await waitFor(() => {
118 | expect(screen.getByText(/3 Products/i)).toBeInTheDocument();
119 | });
120 |
121 | const form = screen.getByRole('form');
122 | const input = screen.getByRole('searchbox');
123 |
124 | await userEvent.type(input, searchTerm);
125 | await fireEvent.submit(form);
126 |
127 | await waitFor(() => {
128 | expect(screen.getByText(/1 Product$/i)).toBeInTheDocument();
129 | });
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import '../styles/globals.css';
3 |
4 | import Cart from '../components/cart';
5 | import { useCartStore } from '../store/cart';
6 |
7 | if (process.env.NODE_ENV === 'development') {
8 | require('../miragejs/server').makeServer();
9 | }
10 |
11 | function MyApp({ Component, pageProps }) {
12 | const toggle = useCartStore((store) => store.actions.toggle);
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
39 |
NY
40 |
41 |
42 | Brand
43 |
44 |
45 |
61 |
62 |
74 |
75 |
76 |
77 |
111 |
112 |
113 |
114 |
115 |
126 |
127 | );
128 | }
129 |
130 | export default MyApp;
131 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useState, userEffect, useEffect } from 'react';
2 | import { useFetchProducts } from '../hooks/use-fetch-products';
3 | import ProductCard from '../components/product-card';
4 | import Search from '../components/search';
5 | import { useCartStore } from '../store/cart';
6 |
7 | export default function Home() {
8 | const { products, error } = useFetchProducts();
9 | const [term, setTerm] = useState('');
10 | const [localProducts, setLocalProducts] = useState([]);
11 | const addToCart = useCartStore((store) => store.actions.add);
12 |
13 | useEffect(() => {
14 | if (term === '') {
15 | setLocalProducts(products);
16 | } else {
17 | setLocalProducts(
18 | products.filter(({ title }) => {
19 | return title.toLowerCase().indexOf(term.toLowerCase()) > -1;
20 | }),
21 | );
22 | }
23 | }, [products, term]);
24 |
25 | const renderProductListOrMessage = () => {
26 | if (localProducts.length === 0 && !error) {
27 | return No products
;
28 | }
29 | return localProducts.map((product) => (
30 |
31 | ));
32 | };
33 |
34 | const renderErrorMessage = () => {
35 | if (!error) {
36 | return null;
37 | }
38 | return Server is down
;
39 | };
40 |
41 | const renderProductQuantity = () => {
42 | return localProducts.length === 1
43 | ? '1 Product'
44 | : `${localProducts.length} Products`;
45 | };
46 |
47 | return (
48 |
49 | setTerm(term)} />
50 |
51 |
Wrist Watch
52 |
53 | {renderProductQuantity()}
54 |
55 |
56 | {renderErrorMessage()}
57 | {renderProductListOrMessage()}
58 |
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vedovelli/curso-javascript-testes-modulo-3/92d4a5dc2b57e1f95cd4b48cc5c7ecba5655bf45/public/favicon.ico
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
--------------------------------------------------------------------------------
/store/cart/cart-store.unit.spec.js:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react-hooks';
2 | import { useCartStore } from './';
3 | import { makeServer } from '../../miragejs/server';
4 |
5 | describe('Cart Store', () => {
6 | let server;
7 | let result;
8 | let add;
9 | let toggle;
10 | let remove;
11 | let removeAll;
12 | let increase;
13 | let decrease;
14 |
15 | beforeEach(() => {
16 | server = makeServer({ environment: 'test' });
17 | result = renderHook(() => useCartStore()).result;
18 | add = result.current.actions.add;
19 | toggle = result.current.actions.toggle;
20 | remove = result.current.actions.remove;
21 | removeAll = result.current.actions.removeAll;
22 | increase = result.current.actions.increase;
23 | decrease = result.current.actions.decrease;
24 | });
25 |
26 | afterEach(() => {
27 | server.shutdown();
28 | act(() => result.current.actions.reset());
29 | });
30 |
31 | it('should return open equals false on initial state', async () => {
32 | expect(result.current.state.open).toBe(false);
33 | });
34 |
35 | it('should return an empty array for products on initial state', () => {
36 | expect(Array.isArray(result.current.state.products)).toBe(true);
37 | expect(result.current.state.products).toHaveLength(0);
38 | });
39 |
40 | it('should add 2 products to the list and open the cart', async () => {
41 | const products = server.createList('product', 2);
42 |
43 | for (const product of products) {
44 | act(() => add(product));
45 | }
46 |
47 | expect(result.current.state.products).toHaveLength(2);
48 | expect(result.current.state.open).toBe(true);
49 | });
50 |
51 | it('should assign 1 as initial quantity on product add()', () => {
52 | const product = server.create('product');
53 |
54 | act(() => {
55 | add(product);
56 | });
57 |
58 | expect(result.current.state.products[0].quantity).toBe(1);
59 | });
60 |
61 | it('should increase quantity', () => {
62 | const product = server.create('product');
63 |
64 | act(() => {
65 | add(product);
66 | increase(product);
67 | });
68 |
69 | expect(result.current.state.products[0].quantity).toBe(2);
70 | });
71 |
72 | it('should increase quantity', () => {
73 | const product = server.create('product');
74 |
75 | act(() => {
76 | add(product);
77 | decrease(product);
78 | });
79 |
80 | expect(result.current.state.products[0].quantity).toBe(0);
81 | });
82 |
83 | it('should NOT decrease below zero', () => {
84 | const product = server.create('product');
85 |
86 | act(() => {
87 | add(product);
88 | decrease(product);
89 | decrease(product);
90 | });
91 |
92 | expect(result.current.state.products[0].quantity).toBe(0);
93 | });
94 |
95 | it('should not add same product twice', () => {
96 | const product = server.create('product');
97 |
98 | act(() => add(product));
99 | act(() => add(product));
100 |
101 | expect(result.current.state.products).toHaveLength(1);
102 | });
103 |
104 | it('should toggle open state', async () => {
105 | expect(result.current.state.open).toBe(false);
106 | expect(result.current.state.products).toHaveLength(0);
107 |
108 | act(() => toggle());
109 | expect(result.current.state.open).toBe(true);
110 |
111 | act(() => toggle());
112 | expect(result.current.state.open).toBe(false);
113 | expect(result.current.state.products).toHaveLength(0);
114 | });
115 |
116 | it('should remove a product from the store', () => {
117 | const [product1, product2] = server.createList('product', 2);
118 |
119 | act(() => {
120 | add(product1);
121 | add(product2);
122 | });
123 |
124 | expect(result.current.state.products).toHaveLength(2);
125 |
126 | act(() => {
127 | remove(product1);
128 | });
129 |
130 | expect(result.current.state.products).toHaveLength(1);
131 | expect(result.current.state.products[0]).toEqual(product2);
132 | });
133 |
134 | it('should not change products in the cart if provided product is not in the array', () => {
135 | const [product1, product2, product3] = server.createList('product', 3);
136 |
137 | act(() => {
138 | add(product1);
139 | add(product2);
140 | });
141 |
142 | expect(result.current.state.products).toHaveLength(2);
143 |
144 | act(() => {
145 | remove(product3);
146 | });
147 |
148 | expect(result.current.state.products).toHaveLength(2);
149 | });
150 |
151 | it('should clear cart', () => {
152 | const products = server.createList('product', 2);
153 |
154 | act(() => {
155 | for (const product of products) {
156 | add(product);
157 | }
158 | });
159 |
160 | expect(result.current.state.products).toHaveLength(2);
161 |
162 | act(() => {
163 | removeAll();
164 | });
165 |
166 | expect(result.current.state.products).toHaveLength(0);
167 | });
168 | });
169 |
--------------------------------------------------------------------------------
/store/cart/index.js:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 | import produce from 'immer';
3 |
4 | const initialState = {
5 | open: false,
6 | products: [],
7 | };
8 |
9 | export const useCartStore = create((set) => {
10 | const setState = (fn) => set(produce(fn));
11 |
12 | return {
13 | state: {
14 | ...initialState,
15 | },
16 | actions: {
17 | toggle() {
18 | setState(({ state }) => {
19 | state.open = !state.open;
20 | });
21 | },
22 | add(product) {
23 | setState(({ state }) => {
24 | const doesntExist = !state.products.find(
25 | ({ id }) => id === product.id,
26 | );
27 |
28 | if (doesntExist) {
29 | if (!product.quantity) {
30 | product.quantity = 1;
31 | }
32 | state.products.push(product);
33 | state.open = true;
34 | }
35 | });
36 | },
37 | increase(product) {
38 | setState(({ state }) => {
39 | const localProduct = state.products.find(
40 | ({ id }) => id === product.id,
41 | );
42 | if (localProduct) {
43 | localProduct.quantity++;
44 | }
45 | });
46 | },
47 | decrease(product) {
48 | setState(({ state }) => {
49 | const localProduct = state.products.find(
50 | ({ id }) => id === product.id,
51 | );
52 | if (localProduct && localProduct.quantity > 0) {
53 | localProduct.quantity--;
54 | }
55 | });
56 | },
57 | remove(product) {
58 | setState(({ state }) => {
59 | const exists = !!state.products.find(({ id }) => id === product.id);
60 |
61 | if (exists) {
62 | state.products = state.products.filter(({ id }) => {
63 | return id !== product.id;
64 | });
65 | }
66 | });
67 | },
68 | removeAll() {
69 | setState(({ state }) => {
70 | state.products = [];
71 | });
72 | },
73 | reset() {
74 | setState((store) => {
75 | store.state = initialState;
76 | });
77 | },
78 | },
79 | };
80 | });
81 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | /* Write your own custom base styles here */
4 |
5 | /* Start purging... */
6 | @tailwind components;
7 | /* Stop purging. */
8 |
9 | /* Write your own custom component styles here */
10 | .btn-blue {
11 | @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
12 | }
13 |
14 | /* Start purging... */
15 | @tailwind utilities;
16 | /* Stop purging. */
17 |
18 | /* Your own custom utilities */
19 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./components/**/*.{js,ts,jsx,tsx}", "./pages/**/*.{js,ts,jsx,tsx}"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | }
8 |
--------------------------------------------------------------------------------