├── .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 | Favorites 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 | 12 | 18 | 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 | Star Wars 40 | 41 | 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 | {text} 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 | 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 | 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 | 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 | Go back 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 | 12 | 18 | 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 | {personName} 41 | Add to favorite 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 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/PersonPage/PersonPhoto/img/favorite.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 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 | 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 | 12 | 20 | 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 | Loader 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 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/UI/UiLoading/img/loader-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/UI/UiLoading/img/loader-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | Not Found 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 | --------------------------------------------------------------------------------