-
{title}
-
{price}$
-
{brand}
-
+
+
+
+ {original_title ? original_title : title}
+ {`, ${yearRelease}`}
+
+
-
Top
+
{vote_average}
);
};
diff --git a/src/components/cards-list/cards-list.scss b/src/components/cards-list/cards-list.scss
index 1bdcc5f..25a4deb 100644
--- a/src/components/cards-list/cards-list.scss
+++ b/src/components/cards-list/cards-list.scss
@@ -1,6 +1,6 @@
.cards-list {
display: flex;
- justify-content: space-between;
+ justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin: 20px 0;
diff --git a/src/components/cards-list/cards-list.test.tsx b/src/components/cards-list/cards-list.test.tsx
index 2b85c2f..9e85e30 100644
--- a/src/components/cards-list/cards-list.test.tsx
+++ b/src/components/cards-list/cards-list.test.tsx
@@ -2,15 +2,22 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import CardsList from './cards-list';
-import { dataGoods } from '../../data/data';
+import { dataMovie } from '../../data/dataMovie';
+import { IMovie } from '../types';
describe('test CardsList component', () => {
- const products = dataGoods.products.slice(8, 16);
+ const itemsMoke: IMovie[] = [dataMovie, dataMovie];
+ const mockFunction = jest.fn();
test('it renders', async () => {
- render(
);
+ render(
);
const cardsList = await waitFor(() => screen.getByTestId('cards-list'));
expect(cardsList).toBeInTheDocument();
});
+
+ test('should be render all Card in CardsList ', () => {
+ render(
);
+ expect(screen.getAllByRole('card-item').length).toBe(itemsMoke.length);
+ });
});
diff --git a/src/components/cards-list/cards-list.tsx b/src/components/cards-list/cards-list.tsx
index 2ede2f0..fd989e9 100644
--- a/src/components/cards-list/cards-list.tsx
+++ b/src/components/cards-list/cards-list.tsx
@@ -3,19 +3,21 @@ import React from 'react';
import './cards-list.scss';
import Card from '../card';
-import { IProduct } from '../types';
+import { IMovie } from '../types';
type CardListProps = {
- products: IProduct[];
+ items: IMovie[];
+ setIsModalOpen: (newValue: boolean) => void;
+ showDetailInfo: (id: number) => void;
};
-const CardList = ({ products }: CardListProps) => {
- const cards = products.map((product) => {
- const { id } = product;
+const CardList = ({ items, setIsModalOpen, showDetailInfo }: CardListProps) => {
+ const cards = items.map((item) => {
+ const { id } = item;
return (
-
+
);
});
diff --git a/src/components/detailInfo/detailInfo.scss b/src/components/detailInfo/detailInfo.scss
new file mode 100644
index 0000000..583894d
--- /dev/null
+++ b/src/components/detailInfo/detailInfo.scss
@@ -0,0 +1,20 @@
+@import '../../scss/constants.scss';
+
+.detail-info {
+ &__title {
+ font-size: 20px;
+ font-weight: 700;
+ color: $accent1-color;
+ margin: 0 50px 10px;
+ }
+
+ &__subtitle {
+ font-size: 18px;
+ font-weight: 700;
+ color: $gray-color;
+ margin: 0 0 10px;
+ }
+
+ &__homepage {
+ }
+}
diff --git a/src/components/detailInfo/detailInfo.tsx b/src/components/detailInfo/detailInfo.tsx
new file mode 100644
index 0000000..4704c37
--- /dev/null
+++ b/src/components/detailInfo/detailInfo.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import './detailInfo.scss';
+import { IMovie } from '../types';
+
+type DetailInfoProps = {
+ info: IMovie | null;
+};
+
+const DetailInfo = ({ info }: DetailInfoProps) => {
+ const { overview, title, homepage } = info!;
+
+ return (
+
+ );
+};
+
+export default DetailInfo;
diff --git a/src/components/detailInfo/index.tsx b/src/components/detailInfo/index.tsx
new file mode 100644
index 0000000..78e92b4
--- /dev/null
+++ b/src/components/detailInfo/index.tsx
@@ -0,0 +1 @@
+export { default } from './detailInfo';
diff --git a/src/components/errorIndicator/errorIndicator.scss b/src/components/errorIndicator/errorIndicator.scss
new file mode 100644
index 0000000..ae4e400
--- /dev/null
+++ b/src/components/errorIndicator/errorIndicator.scss
@@ -0,0 +1,15 @@
+@import '../../scss/constants.scss';
+
+.error-indicator {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: $accent1-color;
+ padding: 40px;
+}
+
+.error-indicator__title {
+ font-weight: 700;
+ font-size: 1.8rem;
+}
\ No newline at end of file
diff --git a/src/components/errorIndicator/errorIndicator.tsx b/src/components/errorIndicator/errorIndicator.tsx
new file mode 100644
index 0000000..8714c3e
--- /dev/null
+++ b/src/components/errorIndicator/errorIndicator.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import './errorIndicator.scss';
+
+const ErrorIndicator = () => {
+ return (
+
+ Ups!
+ something has gone wrong...
+
+ );
+};
+
+export default ErrorIndicator;
diff --git a/src/components/errorIndicator/index.tsx b/src/components/errorIndicator/index.tsx
new file mode 100644
index 0000000..252985a
--- /dev/null
+++ b/src/components/errorIndicator/index.tsx
@@ -0,0 +1 @@
+export { default } from './errorIndicator';
diff --git a/src/components/modal/index.tsx b/src/components/modal/index.tsx
new file mode 100644
index 0000000..06ff79e
--- /dev/null
+++ b/src/components/modal/index.tsx
@@ -0,0 +1 @@
+export { default } from './modal';
diff --git a/src/components/modal/modal.scss b/src/components/modal/modal.scss
new file mode 100644
index 0000000..24f14b9
--- /dev/null
+++ b/src/components/modal/modal.scss
@@ -0,0 +1,44 @@
+@import '../../scss/constants.scss';
+
+.modal {
+ position: fixed;
+ z-index: 10;
+
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(0, 0, 0, 0.8);
+
+ &__content {
+ position: relative;
+ width: 80%;
+ margin: 0 auto;
+ max-width: 440px;
+ border-radius: $border-radius-big;
+ background-color: $light-color-2;
+ color: $text-color;
+ padding: 30px 22px;
+ text-align: center;
+
+ @media (max-width: 480px) {
+ width: 90%;
+ padding: 22px 18px;
+ }
+ }
+
+ &__close-btn {
+ position: absolute;
+ top: 25px;
+ right: 25px;
+ }
+
+ p,
+ li {
+ text-align: left;
+ margin: 0 0 5px 0;
+ }
+}
diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx
new file mode 100644
index 0000000..ede58b2
--- /dev/null
+++ b/src/components/modal/modal.tsx
@@ -0,0 +1,32 @@
+import React, { MouseEvent, ReactElement } from 'react';
+
+import './modal.scss';
+
+type ModalProps = {
+ isModalOpen: boolean;
+ setIsModalOpen: (newValue: boolean) => void;
+ children: ReactElement;
+};
+
+const Modal = ({ setIsModalOpen, children }: ModalProps) => {
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const unCloseModal = (e: MouseEvent
): void => {
+ e.stopPropagation();
+ };
+
+ return (
+
+ );
+};
+
+export default Modal;
diff --git a/src/components/search-panel/search-panel.test.tsx b/src/components/search-panel/search-panel.test.tsx
index e424e2d..a9ce170 100644
--- a/src/components/search-panel/search-panel.test.tsx
+++ b/src/components/search-panel/search-panel.test.tsx
@@ -2,13 +2,14 @@ import { fireEvent, render, screen } from '@testing-library/react';
import SearchPanel from './search-panel';
describe('test SearchPanel component', () => {
+ const onChange = jest.fn();
test('should contains input', () => {
- render();
+ render();
expect(screen.getByRole('search-input')).toBeInTheDocument();
});
test('value of input changes by user', () => {
- render();
+ render();
const input = screen.getByRole('search-input') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'some text for test' } });
expect(input.value).toBe('some text for test');
diff --git a/src/components/search-panel/search-panel.tsx b/src/components/search-panel/search-panel.tsx
index d7cbdb9..b23ac70 100644
--- a/src/components/search-panel/search-panel.tsx
+++ b/src/components/search-panel/search-panel.tsx
@@ -1,8 +1,12 @@
-import React, { FC, ChangeEvent, useState, useEffect, useRef } from 'react';
+import React, { ChangeEvent, KeyboardEvent, useState, useEffect, useRef } from 'react';
import './search-panel.scss';
-const SearchPanel: FC = () => {
+type SearchPanelProps = {
+ updateSearchValue: (searchValue: string) => void;
+};
+
+const SearchPanel = ({ updateSearchValue }: SearchPanelProps) => {
const initSearchValue: string = localStorage.getItem('searchValue') || '';
const [searchValue, setSearchValue] = useState(initSearchValue);
const searchRef = useRef(searchValue);
@@ -10,8 +14,7 @@ const SearchPanel: FC = () => {
useEffect(() => {
//like componentWillUnmount
return function saveToLS() {
- const currentSearchValue = searchRef?.current || '';
- localStorage.setItem('searchValue', currentSearchValue);
+ localStorage.setItem('searchValue', searchRef.current);
};
}, []);
@@ -21,7 +24,13 @@ const SearchPanel: FC = () => {
searchRef.current = newSearchValue;
};
- const searchText = 'Type here to search...';
+ const handleKeyDown = (e: KeyboardEvent): void => {
+ if (e.key === 'Enter') {
+ updateSearchValue(searchValue);
+ }
+ };
+
+ const searchText = 'Type here to search and press Enter...';
return (
{
placeholder={searchText}
value={searchValue}
onChange={(e) => onSearchChange(e)}
+ onKeyDown={handleKeyDown}
role='search-input'
/>
);
diff --git a/src/components/spinner/index.tsx b/src/components/spinner/index.tsx
new file mode 100644
index 0000000..2af5bc0
--- /dev/null
+++ b/src/components/spinner/index.tsx
@@ -0,0 +1 @@
+export { default } from './spinner';
diff --git a/src/components/spinner/spinner.scss b/src/components/spinner/spinner.scss
new file mode 100644
index 0000000..2759855
--- /dev/null
+++ b/src/components/spinner/spinner.scss
@@ -0,0 +1,228 @@
+.spinner__container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ padding: 20px;
+}
+
+@keyframes ldio-vvilw6h75t9 {
+ 0% {
+ opacity: 1;
+ backface-visibility: hidden;
+ transform: translateZ(0) scale(1.5, 1.5);
+ }
+ 100% {
+ opacity: 0;
+ backface-visibility: hidden;
+ transform: translateZ(0) scale(1, 1);
+ }
+}
+.ldio-vvilw6h75t9 div > div {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #5f2a62;
+ animation: ldio-vvilw6h75t9 1s linear infinite;
+}
+.ldio-vvilw6h75t9 div:nth-child(1) > div {
+ left: 150px;
+ top: 90px;
+ animation-delay: -0.95s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(1) {
+ transform: rotate(0deg);
+ transform-origin: 160px 100px;
+}
+.ldio-vvilw6h75t9 div:nth-child(2) > div {
+ left: 147px;
+ top: 109px;
+ animation-delay: -0.9s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(2) {
+ transform: rotate(18deg);
+ transform-origin: 157px 119px;
+}
+.ldio-vvilw6h75t9 div:nth-child(3) > div {
+ left: 139px;
+ top: 125px;
+ animation-delay: -0.85s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(3) {
+ transform: rotate(36deg);
+ transform-origin: 149px 135px;
+}
+.ldio-vvilw6h75t9 div:nth-child(4) > div {
+ left: 125px;
+ top: 139px;
+ animation-delay: -0.8s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(4) {
+ transform: rotate(54deg);
+ transform-origin: 135px 149px;
+}
+.ldio-vvilw6h75t9 div:nth-child(5) > div {
+ left: 109px;
+ top: 147px;
+ animation-delay: -0.75s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(5) {
+ transform: rotate(72deg);
+ transform-origin: 119px 157px;
+}
+.ldio-vvilw6h75t9 div:nth-child(6) > div {
+ left: 90px;
+ top: 150px;
+ animation-delay: -0.7s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(6) {
+ transform: rotate(90deg);
+ transform-origin: 100px 160px;
+}
+.ldio-vvilw6h75t9 div:nth-child(7) > div {
+ left: 71px;
+ top: 147px;
+ animation-delay: -0.65s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(7) {
+ transform: rotate(108deg);
+ transform-origin: 81px 157px;
+}
+.ldio-vvilw6h75t9 div:nth-child(8) > div {
+ left: 55px;
+ top: 139px;
+ animation-delay: -0.6s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(8) {
+ transform: rotate(126deg);
+ transform-origin: 65px 149px;
+}
+.ldio-vvilw6h75t9 div:nth-child(9) > div {
+ left: 41px;
+ top: 125px;
+ animation-delay: -0.55s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(9) {
+ transform: rotate(144deg);
+ transform-origin: 51px 135px;
+}
+.ldio-vvilw6h75t9 div:nth-child(10) > div {
+ left: 33px;
+ top: 109px;
+ animation-delay: -0.5s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(10) {
+ transform: rotate(162deg);
+ transform-origin: 43px 119px;
+}
+.ldio-vvilw6h75t9 div:nth-child(11) > div {
+ left: 30px;
+ top: 90px;
+ animation-delay: -0.45s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(11) {
+ transform: rotate(180deg);
+ transform-origin: 40px 100px;
+}
+.ldio-vvilw6h75t9 div:nth-child(12) > div {
+ left: 33px;
+ top: 71px;
+ animation-delay: -0.4s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(12) {
+ transform: rotate(198deg);
+ transform-origin: 43px 81px;
+}
+.ldio-vvilw6h75t9 div:nth-child(13) > div {
+ left: 41px;
+ top: 55px;
+ animation-delay: -0.35s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(13) {
+ transform: rotate(216deg);
+ transform-origin: 51px 65px;
+}
+.ldio-vvilw6h75t9 div:nth-child(14) > div {
+ left: 55px;
+ top: 41px;
+ animation-delay: -0.3s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(14) {
+ transform: rotate(234deg);
+ transform-origin: 65px 51px;
+}
+.ldio-vvilw6h75t9 div:nth-child(15) > div {
+ left: 71px;
+ top: 33px;
+ animation-delay: -0.25s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(15) {
+ transform: rotate(252deg);
+ transform-origin: 81px 43px;
+}
+.ldio-vvilw6h75t9 div:nth-child(16) > div {
+ left: 90px;
+ top: 30px;
+ animation-delay: -0.2s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(16) {
+ transform: rotate(270deg);
+ transform-origin: 100px 40px;
+}
+.ldio-vvilw6h75t9 div:nth-child(17) > div {
+ left: 109px;
+ top: 33px;
+ animation-delay: -0.15s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(17) {
+ transform: rotate(288deg);
+ transform-origin: 119px 43px;
+}
+.ldio-vvilw6h75t9 div:nth-child(18) > div {
+ left: 125px;
+ top: 41px;
+ animation-delay: -0.1s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(18) {
+ transform: rotate(306deg);
+ transform-origin: 135px 51px;
+}
+.ldio-vvilw6h75t9 div:nth-child(19) > div {
+ left: 139px;
+ top: 55px;
+ animation-delay: -0.05s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(19) {
+ transform: rotate(324deg);
+ transform-origin: 149px 65px;
+}
+.ldio-vvilw6h75t9 div:nth-child(20) > div {
+ left: 147px;
+ top: 71px;
+ animation-delay: 0s;
+}
+.ldio-vvilw6h75t9 > div:nth-child(20) {
+ transform: rotate(342deg);
+ transform-origin: 157px 81px;
+}
+.loadingio-spinner-spin-lw0dq5kmlqd {
+ width: 200px;
+ height: 200px;
+ display: inline-block;
+ overflow: hidden;
+ background: none;
+}
+.ldio-vvilw6h75t9 {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ transform: translateZ(0) scale(1);
+ backface-visibility: hidden;
+ transform-origin: 0 0; /* see note above */
+}
+.ldio-vvilw6h75t9 div {
+ box-sizing: content-box;
+}
+
+/* generated by https://loading.io/ */
diff --git a/src/components/spinner/spinner.tsx b/src/components/spinner/spinner.tsx
new file mode 100644
index 0000000..0a0c13d
--- /dev/null
+++ b/src/components/spinner/spinner.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+
+import './spinner.scss';
+
+const Spinner = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Spinner;
diff --git a/src/components/types.tsx b/src/components/types.tsx
index 83c327f..98c78bc 100644
--- a/src/components/types.tsx
+++ b/src/components/types.tsx
@@ -1,15 +1,15 @@
export interface IProduct {
id: number;
title: string;
- description: string;
- price: number;
+ description?: string;
+ price?: number;
discountPercentage?: number;
rating?: number;
stock?: number;
- brand: string;
+ brand?: string;
category?: string;
thumbnail?: string;
- images: string[];
+ images?: string[];
}
export type DataGoods = {
@@ -18,3 +18,23 @@ export type DataGoods = {
skip: number;
limit: number;
};
+
+//https://developers.themoviedb.org/3/trending/get-trending
+export interface IMovie {
+ adult?: boolean;
+ backdrop_path?: string | null;
+ genre_ids?: number[];
+ id: number;
+ media_type?: string;
+ original_language?: string;
+ original_title: string;
+ overview: string;
+ popularity?: number;
+ poster_path: string | null;
+ release_date: string;
+ title: string;
+ video?: boolean;
+ vote_average: number;
+ vote_count?: number;
+ homepage?: string | null;
+}
diff --git a/src/data/dataMovie.ts b/src/data/dataMovie.ts
new file mode 100644
index 0000000..b4be481
--- /dev/null
+++ b/src/data/dataMovie.ts
@@ -0,0 +1,14 @@
+import { IMovie } from '../components/types';
+
+export const dataMovie: IMovie = {
+ adult: false,
+ id: 550,
+ original_language: 'en',
+ original_title: 'Fight Club',
+ overview:
+ 'A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.',
+ poster_path: '/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg',
+ release_date: '1999-10-15',
+ title: 'Fight Club',
+ vote_average: 8.431,
+};
diff --git a/src/data/dataProduct.ts b/src/data/dataProduct.ts
new file mode 100644
index 0000000..a2ddf94
--- /dev/null
+++ b/src/data/dataProduct.ts
@@ -0,0 +1,17 @@
+export const product = {
+ id: 22,
+ title: 'Elbow Macaroni - 400 gm',
+ description: 'Product details of Bake Parlor Big Elbow Macaroni - 400 gm',
+ price: 14,
+ discountPercentage: 15.58,
+ rating: 4.57,
+ stock: 146,
+ brand: 'Bake Parlor Big',
+ category: 'groceries',
+ thumbnail: 'https://i.dummyjson.com/data/products/22/thumbnail.jpg',
+ images: [
+ 'https://i.dummyjson.com/data/products/22/1.jpg',
+ 'https://i.dummyjson.com/data/products/22/2.jpg',
+ 'https://i.dummyjson.com/data/products/22/3.jpg',
+ ],
+};
diff --git a/src/data/data.ts b/src/data/dataProducts.ts
similarity index 100%
rename from src/data/data.ts
rename to src/data/dataProducts.ts
diff --git a/src/mocks/handlers.tsx b/src/mocks/handlers.tsx
new file mode 100644
index 0000000..5d589a7
--- /dev/null
+++ b/src/mocks/handlers.tsx
@@ -0,0 +1,42 @@
+import { rest } from 'msw';
+import { dataMovie } from '../data/dataMovie';
+
+export const handlers = [
+ rest.post('/login', (req, res, ctx) => {
+ // Persist user's authentication in the session
+ sessionStorage.setItem('is-authenticated', 'true');
+
+ return res(
+ // Respond with a 200 status code
+ ctx.status(200)
+ );
+ }),
+
+ rest.get('https://api.themoviedb.org/3/trending/movie/week', (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ results: [dataMovie, dataMovie, dataMovie],
+ })
+ );
+ }),
+ rest.get('https://api.themoviedb.org/3/trending/movie/day', (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ results: [dataMovie],
+ })
+ );
+ }),
+ rest.get('https://api.themoviedb.org/3/movie/550', (req, res, ctx) => {
+ return res(ctx.status(200), ctx.json(dataMovie));
+ }),
+ rest.get('https://api.themoviedb.org/3/search/movie', (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ results: [dataMovie, dataMovie, dataMovie, dataMovie, dataMovie],
+ })
+ );
+ }),
+];
diff --git a/src/mocks/server.tsx b/src/mocks/server.tsx
new file mode 100644
index 0000000..e52fee0
--- /dev/null
+++ b/src/mocks/server.tsx
@@ -0,0 +1,4 @@
+import { setupServer } from 'msw/node';
+import { handlers } from './handlers';
+
+export const server = setupServer(...handlers);
diff --git a/src/pages/homePage/home-page.test.tsx b/src/pages/homePage/home-page.test.tsx
new file mode 100644
index 0000000..dc91e79
--- /dev/null
+++ b/src/pages/homePage/home-page.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+
+import { server } from './../../mocks/server';
+import { rest } from 'msw';
+
+import HomePage from './homePage';
+import DetailInfo from '../../components/detailInfo';
+import { dataMovie } from '../../data/dataMovie';
+
+describe('test HomePage component', () => {
+ test('it renders', () => {
+ render();
+ expect(screen.getByRole('home-page')).toBeInTheDocument();
+ });
+
+ test('show spinner component', async () => {
+ const { findByTestId } = render();
+ expect(await findByTestId('spinner')).toBeInTheDocument();
+ });
+
+ test('render cards from mocks API ', async () => {
+ render();
+ const expectedLength = 3; //length arrMovies in mock API in mocks/server.tsx
+ const movies = await screen.findAllByRole('card-item');
+ expect(movies).toHaveLength(expectedLength);
+ });
+
+ test('render error ', async () => {
+ server.use(
+ rest.get('https://api.themoviedb.org/3/trending/movie/week', (req, res, ctx) => {
+ return res(ctx.status(500));
+ })
+ );
+ render();
+ const error = await screen.findByTestId('error-indicator');
+ expect(error).toBeInTheDocument();
+ });
+
+ test('render detail-info on Home Page ', async () => {
+ render();
+ const item = screen.getByTestId('detail-info');
+ expect(item).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/homePage/homePage.scss b/src/pages/homePage/homePage.scss
index 5864a42..43a8d9e 100644
--- a/src/pages/homePage/homePage.scss
+++ b/src/pages/homePage/homePage.scss
@@ -3,3 +3,9 @@
padding: 30px 0;
text-align: center;
}
+
+.movies__container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
diff --git a/src/pages/homePage/homePage.tsx b/src/pages/homePage/homePage.tsx
index 00c514e..0f79631 100644
--- a/src/pages/homePage/homePage.tsx
+++ b/src/pages/homePage/homePage.tsx
@@ -1,19 +1,92 @@
-import React, { FC } from 'react';
+import React, { FC, useEffect, useState } from 'react';
import './homePage.scss';
-import { dataGoods } from '../../data/data';
-import { IProduct } from '../../components/types';
+import { IMovie } from '../../components/types';
import CardList from '../../components/cards-list';
import SearchPanel from '../../components/search-panel';
+import { getMovieById, getMoviesBySearch, getTrendingMovies } from '../../services/movies-services';
+import Spinner from '../../components/spinner';
+import ErrorIndicator from '../../components/errorIndicator';
+import Modal from '../../components/modal';
+import DetailInfo from '../../components/detailInfo';
const HomePage: FC = () => {
- const products: IProduct[] = dataGoods.products.slice(0, 8); //get only 8 products
+ const initSearchValue: string = localStorage.getItem('searchValue') || '';
+ const [searchValue, setSearchValue] = useState(initSearchValue);
+ const [trendingMovies, setTrendingMovies] = useState([]);
+ const [movies, setMovies] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [detailInfo, setDetailInfo] = useState(null);
+ const [movieId, setMovieId] = useState(null);
+
+ const onError = () => {
+ setError(true);
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ getTrendingMovies('week')
+ .then((trendingMovies) => {
+ setTrendingMovies(trendingMovies);
+ setLoading(false);
+ })
+ .catch(onError);
+ }, []);
+
+ useEffect(() => {
+ if (searchValue !== '') {
+ getMoviesBySearch(searchValue)
+ .then((movies) => {
+ setMovies(movies);
+ setLoading(false);
+ })
+ .catch(onError);
+ }
+ }, [searchValue]);
+
+ const updateSearchValue = (newValue: string) => {
+ setSearchValue(newValue);
+ };
+
+ function showDetailInfo(id: number) {
+ setLoading(true);
+ setMovieId(id);
+ }
+
+ useEffect(() => {
+ if (isModalOpen && movieId) {
+ getMovieById(movieId)
+ .then((movie) => {
+ setDetailInfo(movie);
+ setLoading(false);
+ })
+ .catch(onError);
+ }
+ }, [movieId, isModalOpen]);
+
+ const hasData = !(loading || error);
+ const errorMessage = error ? : null;
+ const spinner = loading ? : null;
+
+ const searchedMovies = searchValue !== '' ? movies : trendingMovies;
+ const content = hasData ? (
+
+ ) : null;
return (
-
+
HomePage
-
-
+
+ {spinner}
+ {errorMessage}
+ {content}
+ {isModalOpen && hasData && (
+
+
+
+ )}
);
};
diff --git a/src/scss/base.scss b/src/scss/base.scss
index d093164..059a5ef 100644
--- a/src/scss/base.scss
+++ b/src/scss/base.scss
@@ -72,6 +72,18 @@ a:active {
}
}
+.btn--round {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 30px;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ line-height: 1;
+ font-size: 14px;
+}
+
.btn--cart {
min-width: auto;
width: 30px;
diff --git a/src/services/api.test.tsx b/src/services/api.test.tsx
new file mode 100644
index 0000000..968910f
--- /dev/null
+++ b/src/services/api.test.tsx
@@ -0,0 +1,26 @@
+import { getMovieById, getMoviesBySearch, getTrendingMovies } from './movies-services';
+
+describe('test API component with mws', () => {
+ it('receives all requested data from Api "https://api.themoviedb.org/3/trending/movie/week" ', async () => {
+ const expectedLength = 3;
+ const data = await getTrendingMovies('week');
+ expect(data).toHaveLength(expectedLength);
+ });
+
+ it('receives all requested data from Api "https://api.themoviedb.org/3/trending/movie/day" ', async () => {
+ const expectedLength = 1;
+ const data = await getTrendingMovies('day');
+ expect(data).toHaveLength(expectedLength);
+ });
+
+ it('receive requested movie-data from Api "https://api.themoviedb.org/3/movie/550" ', async () => {
+ const movie = await getMovieById(550);
+ expect(movie).toBeTruthy();
+ });
+
+ it('receives all requested data from Api "https://api.themoviedb.org/3/search/movie" ', async () => {
+ const expectedLength = 5;
+ const data = await getMoviesBySearch('avatar');
+ expect(data).toHaveLength(expectedLength);
+ });
+});
diff --git a/src/services/movies-services.tsx b/src/services/movies-services.tsx
new file mode 100644
index 0000000..4681415
--- /dev/null
+++ b/src/services/movies-services.tsx
@@ -0,0 +1,43 @@
+import { IMovie } from '../components/types';
+
+const _apiBase = 'https://api.themoviedb.org/3';
+const _apiKey = '75b017a3a227731c05610048a94948e5';
+export const _baseImagePath = 'image.tmdb.org/t/p/w300';
+const _lang = 'en-US';
+
+const getResource = async (url: string) => {
+ const res = await fetch(`${_apiBase}${url}?api_key=${_apiKey}`);
+
+ //handle all answers, except 200 ok
+ if (!res.ok) {
+ throw new Error(`Could not fetch ${url}, received ${res.status}`);
+ }
+ return await res.json();
+};
+
+export const getTrendingMovies = async (time: 'week' | 'day'): Promise
=> {
+ const res = await getResource(`/trending/movie/${time}`);
+ const trendingMovies: IMovie[] = res.results;
+ return trendingMovies;
+};
+
+export const getMovieById = async (movie_id: number): Promise => {
+ const res = await getResource(`/movie/${movie_id}`);
+ const movie: IMovie = res;
+ return movie;
+};
+
+//https://api.themoviedb.org/3/search/movie?api_key=75b017a3a227731c05610048a94948e5&query=avatar&page=1&include_adult=false
+export const getMoviesBySearch = async (searchText: string): Promise => {
+ const res = await fetch(
+ `${_apiBase}/search/movie?api_key=${_apiKey}&language=${_lang}&query=${searchText}}&include_adult=false`
+ );
+
+ //handle all answers, except 200 ok
+ if (!res.ok) {
+ throw new Error(`Could not fetch ${searchText} movies, received ${res.status}`);
+ }
+ const data = await res.json();
+ const movies: IMovie[] = await data.results;
+ return movies;
+};
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 8f2609b..c60987a 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -3,3 +3,9 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+
+import { server } from './mocks/server';
+
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());