├── .gitignore
├── .prettierignore
├── .storybook
├── main.js
└── preview.js
├── CHANGELOG.md
├── README.md
├── config-overrides.js
├── package.json
├── public
├── favicon.png
└── index.html
└── src
├── components
├── ErrorMessage
│ ├── ErrorMessage.jsx
│ ├── ErrorMessage.module.css
│ ├── index.js
│ └── video
│ │ └── han-solo.mp4
├── Favorite
│ ├── Favorite.jsx
│ ├── Favorite.module.css
│ ├── img
│ │ └── bookmark.svg
│ └── index.js
├── Header
│ ├── Header.jsx
│ ├── Header.module.css
│ ├── img
│ │ ├── droid.svg
│ │ ├── lightsaber.svg
│ │ └── space-station.svg
│ └── index.js
├── HomePage
│ └── ChooseSide
│ │ ├── ChooseSide.jsx
│ │ ├── ChooseSide.module.css
│ │ ├── img
│ │ ├── dark-side.jpg
│ │ ├── falcon.jpg
│ │ └── light-side.jpg
│ │ └── index.js
├── PeoplePage
│ ├── PeopleList
│ │ ├── PeopleList.jsx
│ │ ├── PeopleList.module.css
│ │ └── index.js
│ └── PeopleNavigation
│ │ ├── PeopleNavigation.jsx
│ │ ├── PeopleNavigation.module.css
│ │ └── index.js
├── PersonPage
│ ├── PersonFilms
│ │ ├── PersonFilms.jsx
│ │ ├── PersonFilms.module.css
│ │ └── index.js
│ ├── PersonInfo
│ │ ├── PersonInfo.jsx
│ │ ├── PersonInfo.module.css
│ │ └── index.js
│ ├── PersonLinkBack
│ │ ├── PersonLinkBack.jsx
│ │ ├── PersonLinkBack.module.css
│ │ ├── img
│ │ │ └── back.svg
│ │ └── index.js
│ └── PersonPhoto
│ │ ├── PersonPhoto.jsx
│ │ ├── PersonPhoto.module.css
│ │ ├── img
│ │ ├── favorite-fill.svg
│ │ └── favorite.svg
│ │ └── index.js
├── SearchPage
│ └── SearchPageInfo
│ │ ├── SearchPageInfo.jsx
│ │ ├── SearchPageInfo.module.css
│ │ └── index.js
└── UI
│ ├── UiButton
│ ├── UiButton.jsx
│ ├── UiButton.module.css
│ ├── UiButton.stories.js
│ └── index.js
│ ├── UiInput
│ ├── UiInput.jsx
│ ├── UiInput.module.css
│ ├── UiInput.stories.js
│ ├── img
│ │ └── cancel.svg
│ └── index.js
│ ├── UiLoading
│ ├── UiLoading.jsx
│ ├── UiLoading.module.css
│ ├── UiLoading.stories.js
│ ├── img
│ │ ├── loader-black.svg
│ │ ├── loader-blue.svg
│ │ └── loader-white.svg
│ └── index.js
│ ├── UiVideo
│ ├── UiVideo.jsx
│ ├── UiVideo.module.css
│ ├── UiVideo.stories.js
│ ├── index.js
│ └── video
│ │ └── video.mp4
│ └── index.css
├── constants
├── api.js
└── repo.js
├── containers
├── App
│ ├── App.jsx
│ ├── App.module.css
│ └── index.js
├── FavoritesPage
│ ├── FavoritesPage.jsx
│ ├── FavoritesPage.module.css
│ └── index.js
├── HomePage
│ ├── HomePage.jsx
│ ├── HomePage.module.css
│ └── index.js
├── NotFoundPage
│ ├── NotFoundPage.jsx
│ ├── NotFoundPage.module.css
│ ├── img
│ │ └── not-found.png
│ └── index.js
├── PeoplePage
│ ├── PeoplePage.jsx
│ └── index.js
├── PersonPage
│ ├── PersonPage.jsx
│ ├── PersonPage.module.css
│ └── index.js
└── SearchPage
│ ├── SearchPage.jsx
│ ├── SearchPage.module.css
│ └── index.js
├── context
└── ThemeProvider.jsx
├── hoc-helpers
└── withErrorApi.jsx
├── hooks
└── useQueryParams.js
├── index.js
├── routes
└── routesConfig.js
├── services
├── changeCssVariables.js
└── getPeopleData.js
├── static
└── bg.jpg
├── store
├── actions
│ └── index.js
├── constants
│ └── actionTypes.js
├── reducers
│ ├── favoriteReducer.js
│ └── index.js
└── store.js
├── styles
└── index.css
└── utils
├── localStorage.js
└── network.js
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # dependencies
3 | /node_modules
4 | /.pnp
5 | .pnp.js
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 | /storybook-static
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 |
25 | package-lock.json
26 | .eslintcache
27 | debug.log
28 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | CHANGELOG.md
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
3 | addons: [
4 | "@storybook/addon-links",
5 | "@storybook/addon-essentials",
6 | "@storybook/preset-create-react-app",
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: "^on[A-Z].*" },
3 | };
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## VERSION 1.x
4 |
5 | ### [[v1.1] [2021-07-16]](https://github.com/dev-pandaren/react-star-wars/commit/6d25325918058290dca92822e1df30012736a876)
6 |
7 | Изменения
8 |
9 | - Исправлена функция `getId` для получения ID персонажа по URL: не было проверки url на наличие HTTP или HTTPS
10 |
11 |
12 | ### [[v1.2] [2021-08-09]](https://github.com/dev-pandaren/react-star-wars/commit/c2d59d6606394cddeafabfb47a33596bc2023dde)
13 |
14 | Изменения
15 |
16 | - Изменено название переменной `GUIDE_IMG_EXTESION` > `GUIDE_IMG_EXTENSION`
17 |
18 |
19 | ### [[v1.3] [2022-01-24]](https://github.com/dev-pandaren/react-star-wars/commit/451a21c2ce02e58717ba08f291ff10286c012176)
20 |
21 | Изменения
22 |
23 | - Обновлены все NPM пакеты в package.json
24 | - Обновлен React Router до версии 6
25 | - `useHistory()` заменен на `useNavigate()`
26 | - `BrowserRouter` вынесен на уровень выше
27 | - `` заменен на ``
28 | - `match` заменен на `useParams()`
29 |
30 |
31 | ### [[v1.4] [2022-01-24]](https://github.com/dev-pandaren/react-star-wars/commit/ee58140723211f2052d5b73b8cb74474ac5c4315)
32 |
33 | Изменения
34 |
35 | - Рефакторинг `setErrorApi()`
36 |
37 |
38 | ### [[v1.5] [2022-01-24]](https://github.com/dev-pandaren/react-star-wars/commit/5f7d36e624153fec3b1ecbf02a54f4e29cc8a473)
39 |
40 | Изменения
41 |
42 | - Удален второй аргумент у `slice()` - по умолчанию подставляется длина строки
43 | - Более компактная запись для `setPersonFavorite()`
44 |
45 |
46 | ---
47 |
48 | ## VERSION 2.x
49 |
50 | ### [[v2.1] [2024-12-28]](https://github.com/letscode-dev/react-star-wars/pull/3/commits/9bf316046c8dc98a4c023aae4d4c33476e33943f)
51 |
52 | Изменения
53 |
54 | - Обновлены пакеты в package.json
55 | - Удален пакет `redux-devtools-extension`. Были ошибки при установке
56 | - Мелкие правки в файлах
57 |
58 |
59 | Файлы
60 |
61 | > src\index.js
62 | - Устаревший метод `render` заменен на `createRoot`
63 |
64 | > src\utils\network.js
65 | - Удалена функция `changeHTTP`. Теперь менять "HTTP" на "HTTPS" не нужно, она уже по умолчанию "HTTPS"
66 | - Добавлена обработка ошибок в функцию `makeConcurrentRequest`
67 |
68 | > src\store\store.js
69 | - Удален код для пакета `composeWithDevTools`, т.к. пакет был удален
70 |
71 | > src\constants\api.js
72 | - Удалены константы `HTTPS` и `HTTP`, т.к. не используются
73 | - Адрес в `SWAPI_ROOT` заменен на "https://swapi.py4e.com/api", т.к. "https://swapi.dev/api/" не работает. Когда заработает "https://swapi.dev/api/" можно поменять обратно.
74 |
75 | > src\hooks\useQueryParams.js
76 | - Устаревший метод `useLocation` заменен на `useSearchParams`
77 |
78 | > src\services\getPeopleData.js
79 | - Добавлена функция `getPeopleId`
80 | - Удалены функции `checkProtocol` и `getId`
81 |
82 | > src\components\Favorite\Favorite.jsx
83 | - Скорректировано отображение counter
84 |
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🪐 Star Wars Application
2 |
3 | ## 🐧 Links
4 |
5 | - `Project` https://letscode-dev.github.io/react-star-wars
6 | - `Repository` https://github.com/letscode-dev/react-star-wars
7 | - `StoryBook` https://letscode-dev.github.io/react-star-wars/storybook
8 | - `YouTube Playlist` https://www.youtube.com/playlist?list=PL7cTIfGFrdKkQAWKDu2NdFt5Cx38B-A3i
9 | - `Let's Code` https://letscode-dev.github.io/
10 |
11 | ---
12 |
13 | ## 🐶 Available Scripts and Commands
14 |
15 | ```bash
16 | # Install
17 | npm i # install dependencies
18 | ```
19 |
20 | ```bash
21 | # General
22 | npm run start # run app in the development mode
23 | npm run storybook # run storybook
24 | npm run deploy # deploy app on Github Pages
25 | ```
26 |
27 | ```bash
28 | # Deploy (part of "deploy" script)
29 | npm run build # builds the app for production
30 | npm run build-storybook # storybook build
31 | npm run build-gh-pages # deploy on Github Pages
32 | ```
33 |
34 | ```bash
35 | # Not used
36 | npm run eject # remove the single build dependency
37 | npm run deploy-storybook # storybook deploy
38 | ```
39 |
40 | ---
41 |
42 | ## 🦄 API
43 |
44 | - https://swapi.dev (основное API)
45 | - https://swapi.py4e.com (запасное API - если основное не работает)
46 | - https://starwars-visualguide.com (изображения для API)
47 |
48 | ---
49 |
50 | ## 🐗 Lighthouse Metrics Performance
51 |
52 |
53 |
54 | ---
55 |
56 | ## 🐼 Рассмотренные темы
57 |
58 | React.js
59 |
60 | - Разворачивание приложения с `create-react-app`
61 | - Состояние компонента (хук `useState`)
62 | - Жизненный цикл компонента (хук `useEffect`)
63 | - Context API (хук `useContext`)
64 | - Рефы и DOM (хук `useRef`)
65 | - Мемоизация (хук `useCallback`)
66 | - Создание собственных хуков
67 | - Фрагменты
68 | - Паттерн `Higher-Order Component`
69 | - Паттерн `Подъём состояния`
70 | - Обработка событий
71 | - Controlled Components
72 | - Подключение CSS, `css-modules`, библиотека `classnames`
73 | - Списки и ключи, `Reconciliation Algorithm`
74 | - Отложенная загрузка компонентов `React.lazy()`
75 | - Библиотека `prop-types` для валидации props
76 |
77 |
78 | React Router
79 |
80 | - Базовый роутинг
81 | - URL Parameters
82 | - Query Parameters
83 | - Обработка страницы 404 (Not Found)
84 | - Хуки `useLocation` и `useHistory`
85 |
86 |
87 | Redux
88 |
89 | - Базовая структура react-redux-приложения
90 | - Хуки `useDispatch`, `useSelector`
91 | - Redux Middleware
92 | - Создание асинхронных action с библиотекой `redux-thunk`
93 | - Отслеживание состояния store с `redux-devtools-extension`
94 |
95 |
96 | Общее
97 |
98 | - Задание Alias в React-приложении (библиотека `react-app-rewire-alias`)
99 | - Деплой приложения на GitHub Pages (библиотека `gh-pages`)
100 | - Создание Ui-Kit из визуальных компонентов и публикация в `@storybook`
101 | - Библиотека `lodash` с готовыми функциями
102 | - `Visual Studio Code`. Сниппеты и плагины
103 |
104 |
105 | JavaScript
106 |
107 | - Методы работы с массивами: `map`, `filter`, `forEach`
108 | - Асинхронность: `Promise`, `Async Functions`
109 | - ES6-модули (import и export)
110 | - Оператор разворота для объектов (props для компонента)
111 | - Деструктуризация массивов и объектов
112 | - Тернарные операторы
113 | - Работа с Local Storage
114 | - Работа с API с использованием `Fetch`
115 |
116 |
117 | Вёрстка
118 |
119 | - CSS Custom Properties, изменение через JavaScript
120 | - CSS Filters
121 | - CSS Flexbox
122 | - CSS Multi Columns
123 | - Стилизация скроллбара
124 |
125 |
126 | ---
127 |
128 | ## 🐣 Правила
129 |
130 | Порядок импортов
131 |
132 | - Библиотеки
133 | - Контекст
134 | - HOC
135 | - UI-компоненты
136 | - Компоненты
137 | - Изображения
138 | - Хуки
139 | - Роуты
140 | - Сервисы
141 | - Утилиты
142 | - Константы
143 | - Стили
144 |
145 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { alias } = require("react-app-rewire-alias");
2 |
3 | module.exports = function override(config) {
4 | alias({
5 | "@components": "src/components",
6 | "@ui": "src/components/UI",
7 | "@constants": "src/constants",
8 | "@containers": "src/containers",
9 | "@hoc-helpers": "src/hoc-helpers",
10 | "@hooks": "src/hooks",
11 | "@routes": "src/routes",
12 | "@static": "src/static",
13 | "@styles": "src/styles",
14 | "@utils": "src/utils",
15 | "@store": "src/store",
16 | "@services": "src/services",
17 | "@context": "src/context",
18 | })(config);
19 |
20 | return config;
21 | };
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "star-wars",
3 | "version": "2.0.0",
4 | "homepage": "https://letscode-dev.github.io/react-star-wars/",
5 | "private": true,
6 | "dependencies": {
7 | "classnames": "^2.5.1",
8 | "lodash": "^4.17.21",
9 | "prop-types": "^15.8.1",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-redux": "^8.1.3",
13 | "react-router": "^6.20.0",
14 | "react-router-dom": "^6.20.0",
15 | "react-scripts": "^5.0.1",
16 | "redux": "^5.0.1",
17 | "redux-thunk": "^3.1.0"
18 | },
19 | "devDependencies": {
20 | "@storybook/addon-actions": "^6.5.16",
21 | "@storybook/addon-essentials": "^6.5.16",
22 | "@storybook/addon-links": "^6.5.16",
23 | "@storybook/node-logger": "^6.5.16",
24 | "@storybook/preset-create-react-app": "^4.1.2",
25 | "@storybook/react": "^6.5.16",
26 | "@storybook/storybook-deployer": "^2.8.16",
27 | "gh-pages": "^6.2.0",
28 | "http-proxy-middleware": "^3.0.3",
29 | "react-app-rewire-alias": "^1.1.7",
30 | "react-app-rewired": "^2.2.1"
31 | },
32 | "scripts": {
33 | "start": "react-app-rewired start",
34 | "storybook": "start-storybook -p 6006 -s public",
35 | "deploy": "npm run build && npm run build-gh-pages",
36 | "build": "react-app-rewired build",
37 | "build-storybook": "build-storybook -o ./build/storybook",
38 | "build-gh-pages": "gh-pages -d build",
39 | "deploy-storybook": "storybook-to-ghpages",
40 | "eject": "react-scripts eject"
41 | },
42 | "eslintConfig": {
43 | "extends": [
44 | "react-app",
45 | "react-app/jest"
46 | ]
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Star Wars React
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage/ErrorMessage.jsx:
--------------------------------------------------------------------------------
1 | import UiVideo from "@ui/UiVideo";
2 | import video from "./video/han-solo.mp4";
3 |
4 | import styles from "./ErrorMessage.module.css";
5 |
6 | const ErrorMessage = () => {
7 | return (
8 | <>
9 |
10 | The dark side of the force has won.
11 | We cannot display data.
12 |
13 | Come back when we fix everything
14 |
15 |
16 |
17 | >
18 | );
19 | };
20 |
21 | export default ErrorMessage;
22 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage/ErrorMessage.module.css:
--------------------------------------------------------------------------------
1 | .text {
2 | width: 600px;
3 | margin: auto;
4 | margin-top: 100px;
5 | line-height: 1.7;
6 | font-size: var(--font-size-header);
7 | text-align: center;
8 | text-shadow: 0 0 2px var(--color-white);
9 | color: var(--color-yellow);
10 | -webkit-text-stroke-color: var(--color-yellow);
11 | -webkit-text-stroke-width: 0.5px;
12 | -webkit-text-fill-color: transparent;
13 | }
14 | .video {
15 | display: block;
16 | margin: auto;
17 | margin-top: var(--spacing-large);
18 | border-radius: var(--border-radius-medium);
19 | box-shadow: var(--box-shadow-white);
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ErrorMessage";
2 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage/video/han-solo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/ErrorMessage/video/han-solo.mp4
--------------------------------------------------------------------------------
/src/components/Favorite/Favorite.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 |
5 | import icon from "./img/bookmark.svg";
6 | import styles from "./Favorite.module.css";
7 |
8 | const Favorite = () => {
9 | const [count, setCount] = useState(0);
10 |
11 | const storeData = useSelector((state) => state.favoriteReducer);
12 |
13 | useEffect(() => {
14 | const length = Object.keys(storeData).length;
15 | setCount(length > 99 ? "..." : length);
16 | }, [storeData]);
17 |
18 | return (
19 |
20 |
21 |
{count}
22 |

23 |
24 |
25 | );
26 | };
27 |
28 | export default Favorite;
29 |
--------------------------------------------------------------------------------
/src/components/Favorite/Favorite.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | top: 0;
4 | left: 0;
5 | width: 35px;
6 | height: 35px;
7 | flex-shrink: 0;
8 | margin-left: auto;
9 | }
10 | .icon {
11 | width: 100%;
12 | height: 100%;
13 | object-fit: contain;
14 | object-position: center center;
15 | }
16 | .counter {
17 | position: absolute;
18 | top: -5px;
19 | right: -5px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | width: 20px;
24 | height: 20px;
25 | font-size: 13px;
26 | font-weight: var(--font-bold);
27 | color: var(--color-black);
28 | text-decoration: none;
29 | border-radius: 50%;
30 | background-color: var(--color-white);
31 | box-shadow: var(--box-shadow-white);
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Favorite/img/bookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
19 |
--------------------------------------------------------------------------------
/src/components/Favorite/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Favorite";
2 |
--------------------------------------------------------------------------------
/src/components/Header/Header.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { NavLink } from "react-router-dom";
3 | import {
4 | useTheme,
5 | THEME_LIGHT,
6 | THEME_DARK,
7 | THEME_NEITRAL,
8 | } from "@context/ThemeProvider";
9 | import Favorite from "@components/Favorite";
10 |
11 | import imgSpaceStation from "./img/space-station.svg";
12 | import imgDroid from "./img/droid.svg";
13 | import imgLightsaber from "./img/lightsaber.svg";
14 |
15 | import styles from "./Header.module.css";
16 |
17 | const Header = () => {
18 | const [icon, setIcon] = useState(imgSpaceStation);
19 | const isTheme = useTheme();
20 |
21 | useEffect(() => {
22 | switch (isTheme.theme) {
23 | case THEME_LIGHT:
24 | setIcon(imgLightsaber);
25 | break;
26 | case THEME_DARK:
27 | setIcon(imgSpaceStation);
28 | break;
29 | case THEME_NEITRAL:
30 | setIcon(imgDroid);
31 | break;
32 | default:
33 | setIcon(imgSpaceStation);
34 | }
35 | }, [isTheme]);
36 |
37 | return (
38 |
39 |

40 |
41 |
42 | -
43 | Home
44 |
45 | -
46 | People
47 |
48 | -
49 | Search
50 |
51 | -
52 | Not Found
53 |
54 | -
55 | Fail
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default Header;
65 |
--------------------------------------------------------------------------------
/src/components/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | margin-bottom: var(--spacing-large);
5 | }
6 | .logo {
7 | width: 60px;
8 | height: 60px;
9 | margin-right: var(--spacing-large);
10 | object-fit: contain;
11 | object-position: center center;
12 | }
13 | .list__container {
14 | display: flex;
15 | flex-wrap: wrap;
16 | margin: 0;
17 | padding: 0;
18 | list-style-type: none;
19 | }
20 | .list__container a {
21 | display: block;
22 | margin: 0 10px;
23 | padding: 7px 10px;
24 | min-width: 70px;
25 | border: 2px solid transparent;
26 | border-radius: var(--border-radius-small);
27 | color: var(--color-white);
28 | text-align: center;
29 | font-family: var(--font-family-main);
30 | text-decoration: none;
31 | font-weight: var(--font-bold);
32 | text-shadow: var(--text-shadow-yellow);
33 | transition: var(--transition);
34 | }
35 | .list__container :global(a.active) {
36 | border: 2px solid var(--color-violet);
37 | color: var(--color-violet);
38 | text-shadow: none;
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Header/img/droid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/components/Header/img/lightsaber.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Header/img/space-station.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Header";
2 |
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/ChooseSide.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import cn from "classnames";
3 |
4 | import {
5 | useTheme,
6 | THEME_LIGHT,
7 | THEME_DARK,
8 | THEME_NEITRAL,
9 | } from "@context/ThemeProvider";
10 | import imgLightSide from "./img/light-side.jpg";
11 | import imgDarkSide from "./img/dark-side.jpg";
12 | import imgFalcon from "./img/falcon.jpg";
13 |
14 | import styles from "./ChooseSide.module.css";
15 |
16 | const ChooseSideItem = ({ classes, theme, text, img }) => {
17 | const isTheme = useTheme();
18 |
19 | return (
20 | isTheme.change(theme)}
23 | >
24 |
{text}
25 |

26 |
27 | );
28 | };
29 |
30 | ChooseSideItem.propTypes = {
31 | classes: PropTypes.string,
32 | theme: PropTypes.string,
33 | text: PropTypes.string,
34 | img: PropTypes.string,
35 | };
36 |
37 | const ChooseSide = () => {
38 | const elements = [
39 | {
40 | theme: THEME_LIGHT,
41 | text: "Light Side",
42 | img: imgLightSide,
43 | classes: styles.item__light,
44 | },
45 | {
46 | theme: THEME_DARK,
47 | text: "Dark Side",
48 | img: imgDarkSide,
49 | classes: styles.item__dark,
50 | },
51 | {
52 | theme: THEME_NEITRAL,
53 | text: "I'm Han Solo",
54 | img: imgFalcon,
55 | classes: styles.item__neitral,
56 | },
57 | ];
58 |
59 | return (
60 |
61 | {elements.map(({ theme, text, img, classes }, index) => (
62 |
69 | ))}
70 |
71 | );
72 | };
73 |
74 | export default ChooseSide;
75 |
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/ChooseSide.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --border-radius: 10px;
3 |
4 | --color-light: #29e52d;
5 | --color-dark: #d82228;
6 | --color-neitral: #ffff00;
7 |
8 | --color-shadow-light: rgba(41, 229, 45, 0.4);
9 | --color-shadow-dark: rgba(216, 34, 40, 0.4);
10 | --color-shadow-neitral: rgba(224, 224, 81, 0.4);
11 | }
12 | .container {
13 | display: flex;
14 | flex-wrap: wrap;
15 | margin-top: 40px;
16 | }
17 | .item {
18 | position: relative;
19 | top: 0;
20 | left: 0;
21 | width: 250px;
22 | height: 450px;
23 | margin-right: var(--spacing-medium);
24 | border-radius: var(--border-radius);
25 | box-shadow: var(--box-shadow-black);
26 | transition: var(--transition);
27 | cursor: pointer;
28 | }
29 | .item__header {
30 | position: absolute;
31 | bottom: 10px;
32 | left: 0;
33 | width: 100%;
34 | margin-bottom: var(--spacing-small);
35 | font-size: var(--font-size-subheader);
36 | text-align: center;
37 | }
38 | .item__img {
39 | width: 100%;
40 | height: 100%;
41 | object-fit: cover;
42 | object-position: center center;
43 | border-radius: var(--border-radius);
44 | }
45 |
46 | /***************************************************
47 | COLOR THEME
48 | /***************************************************/
49 | .item__light {
50 | text-shadow: 0 0 5px currentColor;
51 | color: var(--color-light);
52 | }
53 | .item__dark {
54 | text-shadow: 0 0 5px currentColor;
55 | color: var(--color-dark);
56 | }
57 | .item__neitral {
58 | text-shadow: 0 0 5px currentColor;
59 | color: var(--color-neitral);
60 | }
61 |
62 | .item__light:hover {
63 | box-shadow: 0 0 7px 2px var(--color-shadow-light);
64 | }
65 | .item__dark:hover {
66 | box-shadow: 0 0 7px 2px var(--color-shadow-dark);
67 | }
68 | .item__neitral:hover {
69 | box-shadow: 0 0 7px 2px var(--color-shadow-neitral);
70 | }
71 |
72 | @media screen and (max-width: 900px) {
73 | .item {
74 | width: 200px;
75 | height: 350px;
76 | }
77 | }
78 | @media screen and (max-width: 750px) {
79 | .item {
80 | height: 300px;
81 | margin: var(--spacing-small);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/img/dark-side.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/HomePage/ChooseSide/img/dark-side.jpg
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/img/falcon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/HomePage/ChooseSide/img/falcon.jpg
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/img/light-side.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/HomePage/ChooseSide/img/light-side.jpg
--------------------------------------------------------------------------------
/src/components/HomePage/ChooseSide/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ChooseSide";
2 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleList/PeopleList.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { Link } from "react-router-dom";
3 |
4 | import styles from "./PeopleList.module.css";
5 |
6 | const PeopleList = ({ people }) => (
7 |
8 | {people.map(({ id, name, img }) => (
9 | -
10 |
11 |
12 | {name}
13 |
14 |
15 | ))}
16 |
17 | );
18 |
19 | PeopleList.propTypes = {
20 | people: PropTypes.array,
21 | };
22 |
23 | export default PeopleList;
24 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleList/PeopleList.module.css:
--------------------------------------------------------------------------------
1 | .list__container {
2 | display: flex;
3 | flex-wrap: wrap;
4 | margin: 0;
5 | padding: 0;
6 | list-style-type: none;
7 | }
8 | .list__item {
9 | width: 190px;
10 | margin: var(--spacing-medium);
11 | border-radius: var(--border-radius-small);
12 | background-color: var(--color-white);
13 | cursor: pointer;
14 | }
15 | .list__item a {
16 | text-decoration: none;
17 | color: var(--color-black);
18 | text-align: center;
19 | }
20 | .person__photo {
21 | width: 100%;
22 | height: 260px;
23 | object-fit: cover;
24 | object-position: top center;
25 | border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleList/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PeopleList';
2 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleNavigation/PeopleNavigation.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { Link } from "react-router-dom";
3 |
4 | import UiButton from "@ui/UiButton";
5 |
6 | import styles from "./PeopleNavigation.module.css";
7 |
8 | const PeopleNavigation = ({ getResponse, prevPage, nextPage, counterPage }) => {
9 | const handleChangeNext = () => getResponse(nextPage);
10 | const handleChangePrev = () => getResponse(prevPage);
11 |
12 | return (
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | PeopleNavigation.propTypes = {
29 | getResponse: PropTypes.func,
30 | prevPage: PropTypes.string,
31 | nextPage: PropTypes.string,
32 | counterPage: PropTypes.number,
33 | };
34 |
35 | export default PeopleNavigation;
36 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleNavigation/PeopleNavigation.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | justify-content: center;
4 | }
5 | .buttons {
6 | margin: var(--spacing-small);
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/PeoplePage/PeopleNavigation/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PeopleNavigation";
2 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonFilms/PersonFilms.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useState, useEffect } from "react";
3 |
4 | import { makeConcurrentRequest } from "@utils/network";
5 |
6 | import styles from "./PersonFilms.module.css";
7 |
8 | const PersonFilms = ({ personFilms }) => {
9 | const [filmsName, setFilmsName] = useState([]);
10 |
11 | useEffect(() => {
12 | (async () => {
13 | const response = await makeConcurrentRequest(personFilms);
14 |
15 | setFilmsName(response);
16 | })();
17 | }, [personFilms]);
18 |
19 | return (
20 | <>
21 |
22 |
23 | {filmsName
24 | .sort((a, z) => a.episode_id - z.episode_id)
25 | .map(({ title, episode_id }) => (
26 | -
27 |
28 | Episode {episode_id}
29 |
30 | :
31 | {title}
32 |
33 | ))}
34 |
35 |
36 | >
37 | );
38 | };
39 |
40 | PersonFilms.propTypes = {
41 | personFilms: PropTypes.array,
42 | };
43 |
44 | export default PersonFilms;
45 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonFilms/PersonFilms.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | margin-left: var(--spacing-large);
3 | padding: var(--spacing-medium) 0;
4 | }
5 | .list__container {
6 | margin: 0;
7 | padding: 0;
8 | list-style-type: none;
9 | }
10 | .list__item {
11 | display: flex;
12 | align-items: center;
13 | margin-bottom: var(--margin-list);
14 | color: var(--color-white);
15 | }
16 | .item__episode {
17 | padding: var(--spacing-ultrasmall);
18 | border-radius: var(--border-radius-small);
19 | background-color: var(--color-red);
20 | }
21 | .item__colon {
22 | width: 20px;
23 | font-weight: var(--font-bold);
24 | text-align: center;
25 | }
26 | .item__title {
27 | text-shadow: var(--text-shadow-yellow);
28 | }
29 |
30 | @media screen and (max-width: 1020px) {
31 | .wrapper {
32 | width: 100%;
33 | margin: 0;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonFilms/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PersonFilms";
2 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonInfo/PersonInfo.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | import styles from "./PersonInfo.module.css";
4 |
5 | const PersonInfo = ({ personInfo }) => (
6 | <>
7 |
8 |
9 | {personInfo.map(
10 | ({ title, data }) =>
11 | data && (
12 | -
13 | {title}: {data}
14 |
15 | )
16 | )}
17 |
18 |
19 | >
20 | );
21 |
22 | PersonInfo.propTypes = {
23 | personInfo: PropTypes.array,
24 | };
25 |
26 | export default PersonInfo;
27 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonInfo/PersonInfo.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: 300px;
3 | padding: var(--spacing-medium);
4 | margin-left: var(--spacing-large);
5 | background-color: rgba(0, 0, 0, 0.65);
6 | border-radius: var(--border-radius-medium);
7 | color: var(--color-white);
8 | }
9 | .list__container {
10 | margin: 0;
11 | padding: 0;
12 | list-style-type: none;
13 | }
14 | .list__item {
15 | padding: var(--spacing-ultrasmall);
16 | margin-bottom: var(--margin-list);
17 | }
18 | .item__title {
19 | text-decoration: underline;
20 | text-shadow: var(--text-shadow-blue);
21 | }
22 |
23 | @media screen and (max-width: 700px) {
24 | .wrapper {
25 | width: 100%;
26 | margin: 0;
27 | margin-top: var(--spacing-medium);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonInfo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PersonInfo";
2 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonLinkBack/PersonLinkBack.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import iconBack from "./img/back.svg";
3 |
4 | import styles from "./PersonLinkBack.module.css";
5 |
6 | const PersonLinkBack = () => {
7 | const navigate = useNavigate();
8 |
9 | const handleGoBack = (e) => {
10 | e.preventDefault();
11 | navigate(-1);
12 | };
13 |
14 | return (
15 |
16 |
17 | Go back
18 |
19 | );
20 | };
21 |
22 | export default PersonLinkBack;
23 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonLinkBack/PersonLinkBack.module.css:
--------------------------------------------------------------------------------
1 | .link {
2 | display: inline-flex;
3 | align-items: center;
4 | margin-top: 15px;
5 | font-weight: var(--font-bold);
6 | color: var(--color-yellow);
7 | text-decoration: none;
8 | }
9 | .link__img {
10 | width: 30px;
11 | height: 30px;
12 | margin-right: var(--spacing-small);
13 | object-fit: contain;
14 | object-position: center center;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonLinkBack/img/back.svg:
--------------------------------------------------------------------------------
1 |
2 |
19 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonLinkBack/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PersonLinkBack";
2 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonPhoto/PersonPhoto.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useDispatch } from "react-redux";
3 |
4 | import { setPersonToFavorite, removePersonFromFavorites } from "@store/actions";
5 |
6 | import iconFavorite from "./img/favorite.svg";
7 | import iconFavoriteFill from "./img/favorite-fill.svg";
8 |
9 | import styles from "./PersonPhoto.module.css";
10 |
11 | const PersonPhoto = ({
12 | personId,
13 | personPhoto,
14 | personName,
15 | personFavorite,
16 | setPersonFavorite,
17 | }) => {
18 | const dispatch = useDispatch();
19 |
20 | const dispatchFavoritePeople = () => {
21 | if (personFavorite) {
22 | dispatch(removePersonFromFavorites(personId));
23 | } else {
24 | dispatch(
25 | setPersonToFavorite({
26 | [personId]: {
27 | name: personName,
28 | img: personPhoto,
29 | },
30 | })
31 | );
32 | }
33 |
34 | setPersonFavorite(!personFavorite);
35 | };
36 |
37 | return (
38 | <>
39 |
40 |

41 |

47 |
48 | >
49 | );
50 | };
51 |
52 | PersonPhoto.propTypes = {
53 | personId: PropTypes.string,
54 | personPhoto: PropTypes.string,
55 | personName: PropTypes.string,
56 | personFavorite: PropTypes.bool,
57 | setPersonFavorite: PropTypes.func,
58 | };
59 |
60 | export default PersonPhoto;
61 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonPhoto/PersonPhoto.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | top: 0;
4 | left: 0;
5 | width: 250px;
6 | height: 350px;
7 | }
8 | .photo {
9 | width: 100%;
10 | height: 100%;
11 | border-radius: var(--border-radius-medium);
12 | object-fit: cover;
13 | object-position: top center;
14 | }
15 | .favorite {
16 | position: absolute;
17 | top: -15px;
18 | right: -15px;
19 | width: 35px;
20 | height: 35px;
21 | object-fit: contain;
22 | object-position: center center;
23 | filter: drop-shadow(0px 0px 2px #000);
24 | cursor: pointer;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonPhoto/img/favorite-fill.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonPhoto/img/favorite.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/components/PersonPage/PersonPhoto/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PersonPhoto";
2 |
--------------------------------------------------------------------------------
/src/components/SearchPage/SearchPageInfo/SearchPageInfo.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { Link } from 'react-router-dom';
3 |
4 | import styles from './SearchPageInfo.module.css';
5 |
6 | const SearchPageInfo = ({ people }) => (
7 | <>
8 | {people.length
9 | ? (
10 |
11 | {people.map(({ id, name, img }) =>
12 | -
13 |
14 |
15 | {name}
16 |
17 |
18 | )}
19 |
20 | )
21 | : No results
22 | }
23 | >
24 | )
25 |
26 | SearchPageInfo.propTypes = {
27 | people: PropTypes.array,
28 | }
29 |
30 | export default SearchPageInfo;
31 |
--------------------------------------------------------------------------------
/src/components/SearchPage/SearchPageInfo/SearchPageInfo.module.css:
--------------------------------------------------------------------------------
1 | .list__container {
2 | column-count: 3;
3 | margin: var(--spacing-medium) 0;
4 | padding: 0;
5 | list-style-type: none;
6 | }
7 | .list__item {
8 | /* margin: 0; */
9 | }
10 | .list__item a {
11 | display: inline-flex;
12 | color: var(--color-black);
13 | text-decoration: none;
14 | text-align: center;
15 | cursor: pointer;
16 | }
17 | .person__photo {
18 | width: 70px;
19 | height: 70px;
20 | object-fit: cover;
21 | object-position: top center;
22 | border-radius: var(--border-radius-small);
23 | }
24 | .person__name {
25 | display: flex;
26 | align-items: center;
27 | margin: 0;
28 | padding: 0 var(--spacing-small);
29 | color: var(--color-white);
30 | text-shadow: var(--text-shadow-blue);
31 | }
32 | .person__comment {
33 | color: var(--color-white);
34 | }
35 |
36 | @media screen and (max-width: 850px) {
37 | .list__container {
38 | column-count: 2;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/SearchPage/SearchPageInfo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchPageInfo";
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiButton/UiButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import cn from 'classnames';
3 |
4 | import '../index.css';
5 | import styles from './UiButton.module.css';
6 |
7 | const UiButton = ({
8 | text,
9 | onClick,
10 | disabled,
11 | theme='dark',
12 | classes
13 | }) => (
14 |
21 | )
22 |
23 | UiButton.propTypes = {
24 | text: PropTypes.string,
25 | onClick: PropTypes.func,
26 | disabled: PropTypes.bool,
27 | theme: PropTypes.string,
28 | classes: PropTypes.string,
29 | }
30 |
31 | export default UiButton;
32 |
33 | // import UiButton from '../../components/UiButton';
34 | // const handleButtonClick = () => {
35 | // console.log('click');
36 | // }
37 | //
38 |
--------------------------------------------------------------------------------
/src/components/UI/UiButton/UiButton.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | min-width: 100px;
3 | padding: 10px 10px;
4 | border-radius: 3px;
5 | font-size: var(--font-size-small);
6 | font-family: var(--font-family-main);
7 | font-weight: var(--font-bold);
8 | background: none;
9 | outline: none;
10 | transition: var(--transition);
11 | cursor: pointer;
12 | }
13 | .button:disabled,
14 | .button:disabled:hover {
15 | color: var(--color-gray);
16 | background-color: transparent;
17 | border-color: var(--color-gray);
18 | cursor: auto;
19 | }
20 |
21 | /***************************************************
22 | THEME
23 | /***************************************************/
24 | .dark {
25 | border: 1px solid var(--color-white);
26 | color: var(--color-white);
27 | }
28 | .dark:hover {
29 | color: var(--color-black);
30 | background-color: var(--color-white);
31 | }
32 |
33 | .light {
34 | border: 1px solid var(--color-black);
35 | color: var(--color-black);
36 | }
37 | .light:hover {
38 | color: var(--color-white);
39 | background-color: var(--color-black);
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/UI/UiButton/UiButton.stories.js:
--------------------------------------------------------------------------------
1 | import UiButton from "./UiButton";
2 |
3 | export default {
4 | title: "Ui-Kit/UiButton",
5 | component: UiButton,
6 | };
7 |
8 | const Template = (args) => ;
9 |
10 | const props = {
11 | text: "Hello",
12 | onClick: () => console.log("Button Click"),
13 | disabled: false,
14 | theme: "light",
15 | classes: "",
16 | };
17 |
18 | export const Light = Template.bind({});
19 | Light.args = {
20 | ...props,
21 | theme: "light",
22 | };
23 |
24 | export const Dark = Template.bind({});
25 | Dark.args = {
26 | ...props,
27 | theme: "dark",
28 | };
29 |
30 | export const Disabled = Template.bind({});
31 | Disabled.args = {
32 | ...props,
33 | disabled: true,
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/UI/UiButton/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UiButton';
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiInput/UiInput.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import cn from "classnames";
3 |
4 | import icon from "./img/cancel.svg";
5 |
6 | import "../index.css";
7 | import styles from "./UiInput.module.css";
8 |
9 | const UiInput = ({ value, handleInputChange, placeholder, classes }) => (
10 |
11 |
handleInputChange(e.target.value)}
15 | placeholder={placeholder}
16 | className={styles.input}
17 | />
18 |
![]()
value && handleInputChange("")}
20 | src={icon}
21 | className={cn(styles.clear, !value && styles.clear__disabled)}
22 | alt="Clear"
23 | />
24 |
25 | );
26 |
27 | UiInput.propTypes = {
28 | value: PropTypes.string,
29 | handleInputChange: PropTypes.func,
30 | placeholder: PropTypes.string,
31 | classes: PropTypes.string,
32 | };
33 |
34 | export default UiInput;
35 |
36 | // import { useState } from 'react';
37 | // import UiInput from '@ui/UiInput';
38 |
39 | // const App = () => {
40 | // const [value, setValue] = useState('');
41 |
42 | // const handleInputChange = value => {
43 | // setValue(value);
44 | // }
45 |
46 | // return (
47 | //
51 | // )
52 | // }
53 |
--------------------------------------------------------------------------------
/src/components/UI/UiInput/UiInput.module.css:
--------------------------------------------------------------------------------
1 | .wrapper__input {
2 | --border-radius: 3px;
3 | --border: 3px;
4 | --clear-width: 35px;
5 | }
6 |
7 | .wrapper__input {
8 | position: relative;
9 | display: inline-flex;
10 | }
11 | .input {
12 | min-width: 300px;
13 | padding: 10px 15px;
14 | padding-right: calc(var(--clear-width) + 15px);
15 | border: var(--border) solid;
16 | border-color: var(--color-lightgray);
17 | border-radius: var(--border-radius);
18 | font-size: var(--font-size-medium);
19 | font-family: var(--font-family-main);
20 | outline: none;
21 | }
22 | .input:focus {
23 | border-color: var(--color-blue);
24 | }
25 | .clear {
26 | position: absolute;
27 | top: var(--border);
28 | right: var(--border);
29 | width: var(--clear-width);
30 | height: calc(100% - var(--border) * 2);
31 | padding: 10px;
32 | border: none;
33 | outline: none;
34 | object-fit: contain;
35 | object-position: center center;
36 | transition: 0.5s;
37 | opacity: 0.4;
38 | cursor: pointer;
39 | }
40 | .clear__disabled {
41 | opacity: 0.2;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/UI/UiInput/UiInput.stories.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import UiInput from "./UiInput";
3 |
4 | export default {
5 | title: "Ui-Kit/UiInput",
6 | component: UiInput,
7 | };
8 |
9 | const Template = (args) => {
10 | const [value, setValue] = useState("");
11 |
12 | const handleInputChange = (value) => {
13 | setValue(value);
14 | };
15 |
16 | return (
17 |
18 | );
19 | };
20 |
21 | const props = {
22 | value: "",
23 | handleInputChange: () => console.log("Input Change"),
24 | placeholder: "Placeholder",
25 | classes: "",
26 | };
27 |
28 | export const Default = Template.bind({});
29 | Default.args = {
30 | ...props,
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/UI/UiInput/img/cancel.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/src/components/UI/UiInput/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UiInput";
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/UiLoading.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useState, useEffect } from "react";
3 | import cn from "classnames";
4 |
5 | import loaderBlack from "./img/loader-black.svg";
6 | import loaderWhite from "./img/loader-white.svg";
7 | import loaderBlue from "./img/loader-blue.svg";
8 |
9 | import "../index.css";
10 | import styles from "./UiLoading.module.css";
11 |
12 | const UiLoading = ({ theme = "white", isShadow = true, classes }) => {
13 | const [loaderIcon, setLoaderIcon] = useState(null);
14 |
15 | useEffect(() => {
16 | const icons = {
17 | black: loaderBlack,
18 | white: loaderWhite,
19 | blue: loaderBlue,
20 | };
21 | setLoaderIcon(icons[theme] || loaderBlack);
22 | }, [theme]);
23 |
24 | return (
25 |
30 | );
31 | };
32 |
33 | UiLoading.propTypes = {
34 | theme: PropTypes.string,
35 | isShadow: PropTypes.bool,
36 | classes: PropTypes.string,
37 | };
38 |
39 | export default UiLoading;
40 |
41 | // import UiLoading from '@ui/UiLoading';
42 |
43 | // const App = () => {
44 | // return (
45 | //
46 | // )
47 | // }
48 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/UiLoading.module.css:
--------------------------------------------------------------------------------
1 | .loader {
2 | width: 120px;
3 | height: 120px;
4 | }
5 | .shadow {
6 | filter: drop-shadow(0 0 2px #000);
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/UiLoading.stories.js:
--------------------------------------------------------------------------------
1 | import UiLoading from "./UiLoading";
2 |
3 | export default {
4 | title: "Ui-Kit/UiLoading",
5 | component: UiLoading,
6 | };
7 |
8 | const Template = (args) => ;
9 |
10 | const props = {
11 | theme: "black",
12 | isShadow: false,
13 | classes: "",
14 | };
15 |
16 | export const Black = Template.bind({});
17 | Black.args = {
18 | ...props,
19 | theme: "black",
20 | };
21 |
22 | export const White = Template.bind({});
23 | White.args = {
24 | ...props,
25 | theme: "white",
26 | isShadow: true,
27 | };
28 |
29 | export const Blue = Template.bind({});
30 | Blue.args = {
31 | ...props,
32 | theme: "blue",
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/img/loader-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/img/loader-blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/img/loader-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiLoading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UiLoading";
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiVideo/UiVideo.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useEffect, useRef } from "react";
3 | import cn from "classnames";
4 |
5 | import "../index.css";
6 | import styles from "./UiVideo.module.css";
7 |
8 | const UiVideo = ({ src, playbackRate = 1.0, classes }) => {
9 | const videoRef = useRef(null);
10 |
11 | useEffect(() => {
12 | if (videoRef.current) {
13 | videoRef.current.playbackRate = playbackRate;
14 | }
15 | }, [playbackRate]);
16 |
17 | return (
18 |
27 | );
28 | };
29 |
30 | UiVideo.propTypes = {
31 | src: PropTypes.string,
32 | playbackRate: PropTypes.number,
33 | classes: PropTypes.string,
34 | };
35 |
36 | export default UiVideo;
37 |
38 | // import UiVideo from '@ui/UiVideo';
39 | // import video from './background-star.mp4';
40 |
41 | // const App = () => {
42 | // return (
43 | //
47 | // )
48 | // }
49 |
--------------------------------------------------------------------------------
/src/components/UI/UiVideo/UiVideo.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/UI/UiVideo/UiVideo.module.css
--------------------------------------------------------------------------------
/src/components/UI/UiVideo/UiVideo.stories.js:
--------------------------------------------------------------------------------
1 | import UiVideo from "./UiVideo";
2 | import video from "./video/video.mp4";
3 |
4 | export default {
5 | title: "Ui-Kit/UiVideo",
6 | component: UiVideo,
7 | };
8 |
9 | const Template = (args) => ;
10 |
11 | const props = {
12 | src: video,
13 | playbackRate: 1,
14 | classes: "",
15 | };
16 |
17 | export const Default = Template.bind({});
18 | Default.args = {
19 | ...props,
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/UI/UiVideo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UiVideo";
2 |
--------------------------------------------------------------------------------
/src/components/UI/UiVideo/video/video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/components/UI/UiVideo/video/video.mp4
--------------------------------------------------------------------------------
/src/components/UI/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-white: #fff;
3 | --color-lightgray: #dfe2df;
4 | --color-gray: #9c9c9c;
5 | --color-blue: #6a83fb;
6 | --color-black: #000;
7 |
8 | --font-size-small: 14px;
9 | --font-size-medium: 16px;
10 |
11 | --font-bold: 600;
12 | --font-family-main: sans-serif;
13 | --transition: 0.4s;
14 | }
15 |
16 | *,
17 | *:after,
18 | *:before {
19 | box-sizing: border-box;
20 | }
21 | img {
22 | display: block;
23 | }
24 |
--------------------------------------------------------------------------------
/src/constants/api.js:
--------------------------------------------------------------------------------
1 | // swapi base URL
2 | export const SWAPI_ROOT = "https://swapi.py4e.com/api";
3 | export const SWAPI_PEOPLE = "/people";
4 | export const SWAPI_PARAM_PAGE = "/?page=";
5 | export const SWAPI_PARAM_SEARCH = "/?search=";
6 |
7 | export const API_PEOPLE = SWAPI_ROOT + SWAPI_PEOPLE + SWAPI_PARAM_PAGE;
8 | export const API_PERSON = SWAPI_ROOT + SWAPI_PEOPLE;
9 | export const API_SEARCH = SWAPI_ROOT + SWAPI_PEOPLE + SWAPI_PARAM_SEARCH;
10 |
11 | // visualguide
12 | const GUIDE_ROOT_IMG = "https://starwars-visualguide.com/assets/img/";
13 | const GUIDE_PEOPLE = "characters";
14 | export const GUIDE_IMG_EXTENSION = ".jpg";
15 |
16 | export const URL_IMG_PERSON = GUIDE_ROOT_IMG + GUIDE_PEOPLE;
17 |
--------------------------------------------------------------------------------
/src/constants/repo.js:
--------------------------------------------------------------------------------
1 | export const REPO_NAME = "react-star-wars";
2 |
--------------------------------------------------------------------------------
/src/containers/App/App.jsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from "react-router-dom";
2 | import Header from "@components/Header";
3 | import routesConfig from "@routes/routesConfig";
4 |
5 | import styles from "./App.module.css";
6 |
7 | const App = () => {
8 | return (
9 |
10 |
11 |
12 | {routesConfig.map((route, index) => (
13 |
14 | ))}
15 |
16 |
17 | );
18 | };
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/containers/App/App.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | max-width: var(--width-max);
3 | margin: auto;
4 | padding: var(--spacing-large) 0;
5 | }
6 |
7 | @media screen and (max-width: 1400px) {
8 | .wrapper {
9 | padding: var(--spacing-large) var(--spacing-large);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./App";
2 |
--------------------------------------------------------------------------------
/src/containers/FavoritesPage/FavoritesPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useSelector } from "react-redux";
3 |
4 | import PeopleList from "@components/PeoplePage/PeopleList";
5 |
6 | import styles from "./FavoritesPage.module.css";
7 |
8 | const FavoritesPage = () => {
9 | const [people, setPeople] = useState([]);
10 |
11 | const storeData = useSelector((state) => state.favoriteReducer);
12 |
13 | useEffect(() => {
14 | const favorites = Object.entries(storeData).map(([id, data]) => ({
15 | id,
16 | ...data,
17 | }));
18 | setPeople(favorites);
19 | }, [storeData]);
20 |
21 | return (
22 | <>
23 | Favorites
24 | {people.length ? (
25 |
26 | ) : (
27 | No data
28 | )}
29 | >
30 | );
31 | };
32 |
33 | export default FavoritesPage;
34 |
--------------------------------------------------------------------------------
/src/containers/FavoritesPage/FavoritesPage.module.css:
--------------------------------------------------------------------------------
1 | .comment {
2 | color: var(--color-white);
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/FavoritesPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./FavoritesPage";
2 |
--------------------------------------------------------------------------------
/src/containers/HomePage/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import ChooseSide from "@components/HomePage/ChooseSide";
2 |
3 | // import styles from './HomePage.module.css';
4 |
5 | const HomePage = () => {
6 | return (
7 | <>
8 | Choose your side
9 |
10 | >
11 | );
12 | };
13 |
14 | export default HomePage;
15 |
--------------------------------------------------------------------------------
/src/containers/HomePage/HomePage.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/containers/HomePage/HomePage.module.css
--------------------------------------------------------------------------------
/src/containers/HomePage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HomePage";
2 |
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/NotFoundPage.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from "react-router-dom";
2 | import img from "./img/not-found.png";
3 | import styles from "./NotFoundPage.module.css";
4 |
5 | const NotFoundPage = () => {
6 | let location = useLocation();
7 |
8 | return (
9 | <>
10 |
11 |
12 | No match for {location.pathname}
13 |
14 | >
15 | );
16 | };
17 |
18 | export default NotFoundPage;
19 |
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/NotFoundPage.module.css:
--------------------------------------------------------------------------------
1 | .img {
2 | width: 250px;
3 | margin: auto;
4 | margin-top: 100px;
5 | }
6 | .text {
7 | margin-top: var(--spacing-large);
8 | font-size: var(--font-size-subheader);
9 | color: var(--color-white);
10 | text-align: center;
11 | text-shadow: var(--text-shadow-blue);
12 | }
13 |
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/img/not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/containers/NotFoundPage/img/not-found.png
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./NotFoundPage";
2 |
--------------------------------------------------------------------------------
/src/containers/PeoplePage/PeoplePage.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useState, useEffect, useCallback } from "react";
3 |
4 | import { withErrorApi } from "@hoc-helpers/withErrorApi";
5 | import PeopleNavigation from "@components/PeoplePage/PeopleNavigation";
6 | import PeopleList from "@components/PeoplePage/PeopleList";
7 | import { useQueryParams } from "@hooks/useQueryParams";
8 |
9 | import { getApiResource } from "@utils/network";
10 | import {
11 | getPeopleImage,
12 | getPeopleId,
13 | getPeoplePageId,
14 | } from "@services/getPeopleData";
15 | import { API_PEOPLE } from "@constants/api";
16 |
17 | const PeoplePage = ({ setErrorApi }) => {
18 | const [people, setPeople] = useState(null);
19 | const [prevPage, setPrevPage] = useState(null);
20 | const [nextPage, setNextPage] = useState(null);
21 | const [counterPage, setCounterPage] = useState(1);
22 |
23 | const query = useQueryParams();
24 | const queryPage = query.get("page");
25 |
26 | const getResponse = useCallback(
27 | async (url) => {
28 | const res = await getApiResource(url);
29 |
30 | if (res) {
31 | const peopleList = res.results.map(({ name, url }) => {
32 | const id = getPeopleId(url);
33 | const img = getPeopleImage(id);
34 |
35 | return {
36 | id,
37 | name,
38 | img,
39 | };
40 | });
41 |
42 | setPeople(peopleList);
43 | setNextPage(res.next);
44 | setPrevPage(res.previous);
45 | setCounterPage(getPeoplePageId(url));
46 | }
47 |
48 | setErrorApi(!res);
49 | },
50 | [setErrorApi]
51 | );
52 |
53 | useEffect(() => {
54 | getResponse(API_PEOPLE + (queryPage || "1"));
55 | }, [queryPage, getResponse]);
56 |
57 | return (
58 | <>
59 |
65 | {people && }
66 | >
67 | );
68 | };
69 |
70 | PeoplePage.propTypes = {
71 | setErrorApi: PropTypes.func,
72 | };
73 |
74 | export default withErrorApi(PeoplePage);
75 |
--------------------------------------------------------------------------------
/src/containers/PeoplePage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PeoplePage";
2 |
--------------------------------------------------------------------------------
/src/containers/PersonPage/PersonPage.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { useState, useEffect, useCallback, Suspense } from "react";
3 | import { useParams } from "react-router-dom";
4 | import { useSelector } from "react-redux";
5 |
6 | import { withErrorApi } from "@hoc-helpers/withErrorApi";
7 |
8 | import PersonPhoto from "@components/PersonPage/PersonPhoto";
9 | import PersonInfo from "@components/PersonPage/PersonInfo";
10 | import PersonLinkBack from "@components/PersonPage/PersonLinkBack";
11 |
12 | import UiLoading from "@ui/UiLoading";
13 |
14 | import { getApiResource } from "@utils/network";
15 | import { getPeopleImage } from "@services/getPeopleData";
16 | import { API_PERSON } from "@constants/api";
17 |
18 | import styles from "./PersonPage.module.css";
19 |
20 | const PersonFilms = React.lazy(() =>
21 | import("@components/PersonPage/PersonFilms")
22 | );
23 |
24 | const PersonPage = ({ setErrorApi }) => {
25 | const [personId, setPersonId] = useState(null);
26 | const [personInfo, setPersonInfo] = useState(null);
27 | const [personName, setPersonName] = useState(null);
28 | const [personPhoto, setPersonPhoto] = useState(null);
29 | const [personFilms, setPersonFilms] = useState(null);
30 | const [personFavorite, setPersonFavorite] = useState(false);
31 |
32 | const storeData = useSelector((state) => state.favoriteReducer);
33 |
34 | const { id } = useParams();
35 |
36 | const getPersonData = useCallback(async () => {
37 | setPersonFavorite(!!storeData[id]);
38 | setPersonId(id);
39 |
40 | const res = await getApiResource(`${API_PERSON}/${id}/`);
41 |
42 | if (res) {
43 | setPersonInfo([
44 | { title: "Height", data: res.height },
45 | { title: "Mass", data: res.mass },
46 | { title: "Hair Color", data: res.hair_color },
47 | { title: "Skin Color", data: res.skin_color },
48 | { title: "Eye Color", data: res.eye_color },
49 | { title: "Birth Year", data: res.birth_year },
50 | { title: "Gender", data: res.gender },
51 | ]);
52 | setPersonName(res.name);
53 | setPersonPhoto(getPeopleImage(id));
54 |
55 | if (res.films.length) {
56 | setPersonFilms(res.films);
57 | }
58 | }
59 |
60 | setErrorApi(!res);
61 | }, [id, setErrorApi, storeData]);
62 |
63 | useEffect(() => {
64 | getPersonData();
65 | }, [getPersonData]);
66 |
67 | return (
68 | <>
69 |
70 |
71 |
72 |
{personName}
73 |
74 |
81 |
82 | {personInfo &&
}
83 | {personFilms && (
84 |
}>
85 |
86 |
87 | )}
88 |
89 |
90 | >
91 | );
92 | };
93 |
94 | PersonPage.propTypes = {
95 | setErrorApi: PropTypes.func,
96 | };
97 |
98 | export default withErrorApi(PersonPage);
99 |
--------------------------------------------------------------------------------
/src/containers/PersonPage/PersonPage.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | --color-name: var(--color-violet);
3 | margin-top: 45px;
4 | }
5 | .person__name {
6 | padding: var(--spacing-small);
7 | border-radius: var(--border-radius-small) var(--border-radius-small)
8 | var(--border-radius-small) 0;
9 | font-size: var(--font-size-subheader);
10 | font-weight: var(--font-bold);
11 | color: var(--color-white);
12 | background-color: var(--color-name);
13 | }
14 | .container {
15 | display: flex;
16 | flex-wrap: wrap;
17 | border-left: 5px solid var(--color-name);
18 | padding: var(--spacing-medium);
19 | }
20 |
--------------------------------------------------------------------------------
/src/containers/PersonPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PersonPage";
2 |
--------------------------------------------------------------------------------
/src/containers/SearchPage/SearchPage.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { useState, useEffect, useCallback, useMemo } from "react";
3 | import { debounce } from "lodash";
4 |
5 | import { withErrorApi } from "@hoc-helpers/withErrorApi";
6 | import UiInput from "@ui/UiInput";
7 | import SearchPageInfo from "@components/SearchPage/SearchPageInfo";
8 |
9 | import { getApiResource } from "@utils/network";
10 | import { getPeopleImage, getPeopleId } from "@services/getPeopleData";
11 | import { API_SEARCH } from "@constants/api";
12 |
13 | import styles from "./SearchPage.module.css";
14 |
15 | const SearchPage = ({ setErrorApi }) => {
16 | const [inputSearchValue, setInputSearchValue] = useState("");
17 | const [people, setPeople] = useState([]);
18 |
19 | const getResponse = useCallback(
20 | async (param) => {
21 | const res = await getApiResource(API_SEARCH + param);
22 |
23 | if (res) {
24 | const peopleList = res.results.map(({ name, url }) => {
25 | const id = getPeopleId(url);
26 | const img = getPeopleImage(id);
27 |
28 | return {
29 | id,
30 | name,
31 | img,
32 | };
33 | });
34 |
35 | setPeople(peopleList);
36 | }
37 |
38 | setErrorApi(!res);
39 | },
40 | [setErrorApi]
41 | );
42 |
43 | useEffect(() => {
44 | getResponse("");
45 | }, [getResponse]);
46 |
47 | const debouncedGetResponse = useMemo(
48 | () => debounce((value) => getResponse(value), 300),
49 | [getResponse]
50 | );
51 |
52 | const handleInputChange = useCallback(
53 | (value) => {
54 | setInputSearchValue(value);
55 | debouncedGetResponse(value);
56 | },
57 | [debouncedGetResponse]
58 | );
59 |
60 | // Cleanup debounced function on unmount
61 | useEffect(() => {
62 | return () => {
63 | debouncedGetResponse.cancel();
64 | };
65 | }, [debouncedGetResponse]);
66 |
67 | return (
68 | <>
69 | Search
70 |
71 |
77 |
78 |
79 | >
80 | );
81 | };
82 |
83 | SearchPage.propTypes = {
84 | setErrorApi: PropTypes.func,
85 | };
86 |
87 | export default withErrorApi(SearchPage);
88 |
--------------------------------------------------------------------------------
/src/containers/SearchPage/SearchPage.module.css:
--------------------------------------------------------------------------------
1 | .input__search {
2 | margin-top: 30px;
3 | margin-bottom: 50px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/containers/SearchPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchPage";
2 |
--------------------------------------------------------------------------------
/src/context/ThemeProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 | import { changeCssVariables } from "@services/changeCssVariables";
3 |
4 | export const THEME_LIGHT = "light";
5 | export const THEME_DARK = "dark";
6 | export const THEME_NEITRAL = "neitral";
7 |
8 | const ThemeContext = React.createContext();
9 |
10 | export const ThemeProvider = ({ children, ...props }) => {
11 | const [theme, setTheme] = useState(null);
12 |
13 | const change = (name) => {
14 | setTheme(name);
15 | changeCssVariables(name);
16 | };
17 |
18 | return (
19 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default ThemeProvider;
32 |
33 | export const useTheme = () => useContext(ThemeContext);
34 |
--------------------------------------------------------------------------------
/src/hoc-helpers/withErrorApi.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import ErrorMessage from "@components/ErrorMessage";
3 |
4 | export const withErrorApi = (View) => {
5 | return (props) => {
6 | const [errorApi, setErrorApi] = useState(false);
7 |
8 | return (
9 | <>
10 | {errorApi ? (
11 |
12 | ) : (
13 |
14 | )}
15 | >
16 | );
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/hooks/useQueryParams.js:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from "react-router-dom";
2 |
3 | export const useQueryParams = () => {
4 | const [searchParams] = useSearchParams();
5 | return searchParams;
6 | };
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 | import { Provider } from "react-redux";
5 | import store from "@store/store";
6 |
7 | import { REPO_NAME } from "@constants/repo";
8 |
9 | import ThemeProvider from "@context/ThemeProvider";
10 | import App from "@containers/App";
11 |
12 | import "@styles/index.css";
13 |
14 | const basename = process.env.NODE_ENV === "production" ? `/${REPO_NAME}/` : "/";
15 | const container = document.getElementById("root");
16 | const root = createRoot(container);
17 |
18 | root.render(
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/src/routes/routesConfig.js:
--------------------------------------------------------------------------------
1 | import HomePage from "@containers/HomePage";
2 | import PeoplePage from "@containers/PeoplePage";
3 | import PersonPage from "@containers/PersonPage";
4 | import SearchPage from "@containers/SearchPage";
5 | import FavoritesPage from "@containers/FavoritesPage";
6 | import NotFoundPage from "@containers/NotFoundPage";
7 |
8 | import ErrorMessage from "@components/ErrorMessage";
9 |
10 | const routesConfig = [
11 | {
12 | path: "/",
13 | element: ,
14 | },
15 | {
16 | path: "/people",
17 | element: ,
18 | },
19 | {
20 | path: "/people/:id",
21 | element: ,
22 | },
23 | {
24 | path: "/search",
25 | element: ,
26 | },
27 | {
28 | path: "/favorites",
29 | element: ,
30 | },
31 | {
32 | path: "/fail",
33 | element: ,
34 | },
35 | {
36 | path: "/not-found",
37 | element: ,
38 | },
39 | {
40 | path: "*",
41 | element: ,
42 | },
43 | ];
44 |
45 | export default routesConfig;
46 |
--------------------------------------------------------------------------------
/src/services/changeCssVariables.js:
--------------------------------------------------------------------------------
1 | /*
2 | Формат CSS-переменной:
3 | --theme-default-УникальноеИмя # дефолтная переменная
4 | --theme-light-УникальноеИмя # для "light"
5 | --theme-dark-УникальноеИмя # для "dark"
6 | --theme-neitral-УникальноеИмя # для "neitral"
7 | */
8 |
9 | export const changeCssVariables = (theme) => {
10 | const root = document.querySelector(":root");
11 |
12 | const cssVariables = ["header", "bgimage"];
13 |
14 | cssVariables.forEach((element) => {
15 | root.style.setProperty(
16 | `--theme-default-${element}`,
17 | `var(--theme-${theme}-${element})`
18 | );
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/src/services/getPeopleData.js:
--------------------------------------------------------------------------------
1 | import {
2 | SWAPI_PARAM_PAGE,
3 | SWAPI_ROOT,
4 | SWAPI_PEOPLE,
5 | URL_IMG_PERSON,
6 | GUIDE_IMG_EXTENSION,
7 | } from "@constants/api";
8 |
9 | export const getPeoplePageId = (url) => {
10 | const pos = url.lastIndexOf(SWAPI_PARAM_PAGE);
11 | const id = url.slice(pos + SWAPI_PARAM_PAGE.length);
12 | return Number(id);
13 | };
14 |
15 | export const getPeopleId = (url) => {
16 | const id = url.replace(SWAPI_ROOT + SWAPI_PEOPLE, "").replace(/\//g, "");
17 | return id;
18 | };
19 |
20 | export const getPeopleImage = (id) =>
21 | `${URL_IMG_PERSON}/${id + GUIDE_IMG_EXTENSION}`;
22 |
--------------------------------------------------------------------------------
/src/static/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/letscode-dev/react-star-wars/40edf8c4319399f0a1aed4a78ff5a59f5fccd6e0/src/static/bg.jpg
--------------------------------------------------------------------------------
/src/store/actions/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_PERSON_TO_FAVORITE,
3 | REMOVE_PERSON_FROM_FAVORITE,
4 | } from "@store/constants/actionTypes";
5 |
6 | export const setPersonToFavorite = (person) => ({
7 | type: ADD_PERSON_TO_FAVORITE,
8 | payload: person,
9 | });
10 |
11 | export const removePersonFromFavorites = (personId) => ({
12 | type: REMOVE_PERSON_FROM_FAVORITE,
13 | payload: personId,
14 | });
15 |
16 | // export const setName = (name) => (dispatch) => {
17 | // dispatch({
18 | // type: ADD_PERSON_TO_FAVORITE,
19 | // payload: name
20 | // })
21 | // };
22 |
--------------------------------------------------------------------------------
/src/store/constants/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_PERSON_TO_FAVORITE = "ADD_PERSON_TO_FAVORITE";
2 | export const REMOVE_PERSON_FROM_FAVORITE = "REMOVE_PERSON_FROM_FAVORITE";
3 |
--------------------------------------------------------------------------------
/src/store/reducers/favoriteReducer.js:
--------------------------------------------------------------------------------
1 | import { omit } from "lodash";
2 | import {
3 | ADD_PERSON_TO_FAVORITE,
4 | REMOVE_PERSON_FROM_FAVORITE,
5 | } from "@store/constants/actionTypes";
6 | import { getLocalStorage } from "@utils/localStorage";
7 |
8 | const initialState = getLocalStorage("store");
9 |
10 | const favoriteReducer = (state = initialState, action) => {
11 | switch (action.type) {
12 | case ADD_PERSON_TO_FAVORITE:
13 | return {
14 | ...state,
15 | ...action.payload,
16 | };
17 | case REMOVE_PERSON_FROM_FAVORITE: {
18 | return omit(state, [action.payload]);
19 | }
20 | default:
21 | return state;
22 | }
23 | };
24 |
25 | export default favoriteReducer;
26 |
27 | // case REMOVE_PERSON_FROM_FAVORITE: {
28 | // const { [action.payload]: temp, ...rest } = state;
29 | // return rest;
30 | // }
31 |
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import favoriteReducer from "./favoriteReducer";
3 |
4 | export default combineReducers({
5 | favoriteReducer,
6 | });
7 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { legacy_createStore as createStore, applyMiddleware } from "redux";
2 | import { thunk } from "redux-thunk";
3 | import rootReducer from "./reducers";
4 | import { setLocalStorage } from "@utils/localStorage";
5 |
6 | const store = createStore(rootReducer, applyMiddleware(thunk));
7 |
8 | store.subscribe(() => {
9 | setLocalStorage("store", store.getState().favoriteReducer);
10 | });
11 |
12 | export default store;
13 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | /***************************************************
2 | VARIABLES
3 | /***************************************************/
4 | :root {
5 | --color-white: #fff;
6 | --color-black: #000;
7 | --color-yellow: #ffff00;
8 | --color-blue-light: #e1e9eb;
9 | --color-dark-gray: #10100e;
10 |
11 | --color-blue: #6a83fb;
12 | --color-violet: #ae72c2;
13 | --color-red: #f75d83;
14 |
15 | --spacing-ultrasmall: 5px;
16 | --spacing-small: 10px;
17 | --spacing-medium: 20px;
18 | --spacing-large: 30px;
19 |
20 | --font-size-main: 16px;
21 | --font-size-subheader: 18px;
22 | --font-size-header: 26px;
23 |
24 | --margin-list: 7px;
25 | --width-max: 1200px;
26 | --font-bold: 600;
27 | --transition: 0.4s;
28 | --font-family-main: sans-serif;
29 |
30 | --border-radius-small: 3px;
31 | --border-radius-medium: 5px;
32 |
33 | --text-shadow-yellow: 0 0 5px var(--color-white), 0 0 40px var(--color-yellow);
34 |
35 | --text-shadow-blue: 0 0 2px var(--color-white), 0 0 40px var(--color-blue);
36 |
37 | --box-shadow-white: 0 16px 24px 2px rgba(255, 255, 255, 0.02),
38 | 0 6px 30px 5px rgba(255, 255, 255, 0.04),
39 | 0 8px 10px -5px rgba(255, 255, 255, 0.1);
40 |
41 | --box-shadow-black: 0 16px 24px 2px rgba(0, 0, 0, 0.5),
42 | 0 6px 30px 5px rgba(0, 0, 0, 0.1), 0 8px 10px -5px rgba(0, 0, 0, 0.2);
43 |
44 | /* THEMES */
45 | --theme-light-header: #dfe2df;
46 | --theme-dark-header: #ca6c6f;
47 | --theme-neitral-header: #cccc46;
48 | --theme-default-header: var(--theme-neitral-header);
49 |
50 | --theme-light-bgimage: linear-gradient(
51 | to right bottom,
52 | #051937,
53 | #162454,
54 | #312d70,
55 | #533389,
56 | #7a359f
57 | );
58 | --theme-dark-bgimage: linear-gradient(
59 | to right bottom,
60 | #000000,
61 | #160b11,
62 | #20141e,
63 | #281b2e,
64 | #2b2440
65 | );
66 | --theme-neitral-bgimage: url(@static/bg.jpg);
67 | --theme-default-bgimage: var(--theme-neitral-bgimage);
68 | }
69 |
70 | /***************************************************
71 | GENERAL
72 | /***************************************************/
73 | *,
74 | *:after,
75 | *:before {
76 | box-sizing: border-box;
77 | }
78 | body {
79 | min-height: 100vh;
80 | margin: 0;
81 | padding: 0;
82 | overflow-y: scroll;
83 | font-family: var(--font-family-main);
84 | font-size: var(--font-size-main);
85 | background-color: var(--color-dark-gray);
86 | background-image: var(--theme-default-bgimage);
87 | background-size: cover;
88 | background-repeat: no-repeat;
89 | background-position: top left;
90 | background-attachment: fixed;
91 | }
92 | img {
93 | display: block;
94 | }
95 | .header__text {
96 | line-height: 1.7;
97 | font-size: var(--font-size-header);
98 | text-shadow: 0 0 2px var(--color-black);
99 | color: var(--theme-default-header);
100 | transition: color var(--transition);
101 | }
102 |
103 | /***************************************************
104 | SCROLL
105 | /***************************************************/
106 | /* Firefox */
107 | * {
108 | scrollbar-color: rgb(37, 56, 97) rgba(0, 0, 0, 0.7);
109 | scrollbar-width: thin;
110 | }
111 |
112 | /* Chrome */
113 | ::-webkit-scrollbar {
114 | width: 8px;
115 | height: 8px;
116 | }
117 | ::-webkit-scrollbar-thumb {
118 | background-color: rgb(37, 56, 97);
119 | border-radius: 2px;
120 | transition: var(--transition);
121 | }
122 | ::-webkit-scrollbar-track {
123 | background-color: rgba(0, 0, 0, 0.7);
124 | }
125 | ::-webkit-scrollbar-corner {
126 | background-color: rgba(0, 0, 0, 0.7);
127 | }
128 |
--------------------------------------------------------------------------------
/src/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | export const getLocalStorage = (key) => {
2 | const data = localStorage.getItem(key);
3 |
4 | if (data !== null) {
5 | return JSON.parse(data);
6 | }
7 |
8 | return {};
9 | };
10 |
11 | export const setLocalStorage = (key, data) => {
12 | localStorage.setItem(key, JSON.stringify(data));
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/network.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Отправляет запрос Fetch
3 | * @param {String} url - url для запроса
4 | * @return {Promise} - Promise с результатом запроса
5 | */
6 | export const getApiResource = async (url) => {
7 | try {
8 | const res = await fetch(url);
9 |
10 | if (!res.ok) {
11 | console.error("Could not fetch.", res.status);
12 | return false;
13 | }
14 |
15 | return await res.json();
16 | } catch (error) {
17 | console.error("Could not fetch.", error.message);
18 | return false;
19 | }
20 | };
21 |
22 | export const makeConcurrentRequest = async (urls) => {
23 | try {
24 | const res = await Promise.all(
25 | urls.map((url) => fetch(url).then((res) => res.json()))
26 | );
27 | return res;
28 | } catch (error) {
29 | console.error("Could not fetch concurrent requests.", error.message);
30 | return false;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------