From 54a6e7b156ee37df01d8cc973fcb5fc28bfd1824 Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Sun, 3 Aug 2025 17:23:29 +0300 Subject: [PATCH 1/9] fix: 404 navigate --- src/App.tsx | 2 +- src/MainPage.tsx | 2 +- src/PlanetCard.tsx | 2 +- src/PlanetMiniCard.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index bef00bd..1d1c603 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ function App() { } /> - }> + }> } /> } /> diff --git a/src/MainPage.tsx b/src/MainPage.tsx index dbc9468..3fca380 100644 --- a/src/MainPage.tsx +++ b/src/MainPage.tsx @@ -80,7 +80,7 @@ function MainPage(): React.ReactElement { } function handleSearch() { - navigate(`/1?search=${encodeURIComponent(inputValue)}`); + navigate(`/list/1?search=${encodeURIComponent(inputValue)}`); } function handleInputChange(e: React.ChangeEvent) { diff --git a/src/PlanetCard.tsx b/src/PlanetCard.tsx index bca987f..85e9a30 100644 --- a/src/PlanetCard.tsx +++ b/src/PlanetCard.tsx @@ -14,7 +14,7 @@ function PlanetCard(): React.ReactElement { const navigate = useNavigate(); const handleClose = () => { - navigate(pageNumber ? `/${pageNumber}` : "/"); + navigate(pageNumber ? `/list/${pageNumber}` : "/"); }; useEffect(() => { diff --git a/src/PlanetMiniCard.tsx b/src/PlanetMiniCard.tsx index 422f968..ce45cff 100644 --- a/src/PlanetMiniCard.tsx +++ b/src/PlanetMiniCard.tsx @@ -18,9 +18,9 @@ function PlanetMiniCard({ planet }: PlanetMiniCardProps): React.ReactElement { role="article" onClick={() => { if (!pageNumber) { - navigate(`/1/${id}?search=${searchQuery}`); + navigate(`/list/1/${id}?search=${searchQuery}`); } else { - navigate(`/${pageNumber}/${id}?search=${searchQuery}`); + navigate(`/list/${pageNumber}/${id}?search=${searchQuery}`); } }} > From 14e896cc47cafc1e22fe0b58dfc837913eddd01d Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Mon, 4 Aug 2025 00:46:27 +0300 Subject: [PATCH 2/9] feat: change theme --- src/App.css | 23 +++++++++++++++++++++++ src/App.tsx | 27 +++++++++++++++------------ src/Header.css | 37 +++++++++++++++++++++++++++++++++---- src/Header.tsx | 8 +++++++- src/Input.tsx | 12 +++++++++++- src/MainPage.tsx | 18 ++++++++++++++---- src/Pagination.tsx | 2 +- src/ThemeContext.ts | 11 +++++++++++ src/ThemeProvider.tsx | 25 +++++++++++++++++++++++++ 9 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 src/ThemeContext.ts create mode 100644 src/ThemeProvider.tsx diff --git a/src/App.css b/src/App.css index 11ea586..210c7c8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,11 @@ +body { + background-color: white; +} + +body.dark { + background-color: #2a2a2a; +} + .app-container { max-width: 1200px; margin: 0 auto; @@ -6,6 +14,14 @@ padding-right: 100px; } +.light { + background-color: white; +} + +.dark { + background-color: #2a2a2a; +} + .title { text-align: center; color: #000000; @@ -13,6 +29,10 @@ font-size: 2.5rem; } +.title.dark { + color: #ffffff; +} + .search-container { display: flex; align-items: center; @@ -32,6 +52,9 @@ input { color: white; font-size: 1rem; } +.search-container.dark { + background-color: #0e0e0e; +} .search-container button { padding: 12px 25px; diff --git a/src/App.tsx b/src/App.tsx index 1d1c603..21b157e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,21 +5,24 @@ import About from "./About"; import ErrorBoundary from "./ErrorBoundary"; import { BrowserRouter } from "react-router"; import PlanetCard from "./PlanetCard"; +import { ThemeProvider } from "./ThemeProvider"; function App() { return ( - - - - } /> - }> - } /> - - } /> - } /> - - - + + + + + } /> + }> + } /> + + } /> + } /> + + + + ); } diff --git a/src/Header.css b/src/Header.css index 37ec9df..eeda275 100644 --- a/src/Header.css +++ b/src/Header.css @@ -1,10 +1,13 @@ -header { +header.light { background-color: #2a2a2a; - padding: 20px 30px; - border-radius: 10px; - font-weight: bold; color: #aaa; } + +header.dark { + background-color: #131212; + color: #525151; +} + nav { display: flex; justify-content: start; @@ -22,3 +25,29 @@ nav { text-decoration: none; color: #f8e329; } + +header { + padding: 20px 30px; + border-radius: 10px; + font-weight: bold; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 3%; +} + +.switch_theme_button { + align-self: flex-end; + padding: 12px 25px; + background-color: #f8e329; + color: #000; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; +} + +.switch_theme_button:hover { + background-color: #f1cc10; +} diff --git a/src/Header.tsx b/src/Header.tsx index 9e2b4aa..92cb43b 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -1,8 +1,11 @@ import { NavLink } from "react-router-dom"; import "./Header.css"; +import { ThemeContext } from "./ThemeContext"; +import { useContext } from "react"; function Header() { + const { theme, toggleTheme } = useContext(ThemeContext) || {}; return ( -
+
+
); } diff --git a/src/Input.tsx b/src/Input.tsx index 57205cc..d18c6be 100644 --- a/src/Input.tsx +++ b/src/Input.tsx @@ -1,11 +1,21 @@ import React from "react"; +import { useContext } from "react"; +import { ThemeContext } from "./ThemeContext"; interface InputProps { value: string; onChange: (e: React.ChangeEvent) => void; } function Input({ value, onChange }: InputProps): React.ReactElement { - return ; + const { theme } = useContext(ThemeContext) || {}; + return ( + + ); } export default Input; diff --git a/src/MainPage.tsx b/src/MainPage.tsx index 3fca380..158bae2 100644 --- a/src/MainPage.tsx +++ b/src/MainPage.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useState, useEffect, useCallback } from "react"; +import React, { + useRef, + useState, + useEffect, + useCallback, + useContext, +} from "react"; import { useParams, useSearchParams, @@ -15,6 +21,7 @@ import Input from "./Input"; import type { PlanetsListItem } from "./types"; import Pagination from "./Pagination"; import useLocalStorage from "./hooks/useLocalStorage"; +import { ThemeContext } from "./ThemeContext"; const planetsPerPage = 10; @@ -33,6 +40,9 @@ function MainPage(): React.ReactElement { const [searchPlanets, setPlanets] = useState([]); const [isLoading, setIsLoading] = useState(false); const [totalPlanets, setTotalPlanets] = useState(0); + + const { theme } = useContext(ThemeContext) || {}; + const getPlanets = useCallback(async () => { let errorMessage = "Unknown error"; setIsLoading(true); @@ -92,13 +102,13 @@ function MainPage(): React.ReactElement { } return ( -
+
-

Star Wars Planets

+

Star Wars Planets

{error ?
{error}
: null} -
+
diff --git a/src/Pagination.tsx b/src/Pagination.tsx index ec20f7a..7de7a52 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -24,7 +24,7 @@ export default function Pagination({
{pageNumbers.map((number) => ( void; +}; + +export const ThemeContext = createContext( + {} as ThemeContextType, +); diff --git a/src/ThemeProvider.tsx b/src/ThemeProvider.tsx new file mode 100644 index 0000000..acc785a --- /dev/null +++ b/src/ThemeProvider.tsx @@ -0,0 +1,25 @@ +import React, { useState, useEffect } from "react"; +import { ThemeContext } from "./ThemeContext"; +type Theme = "light" | "dark"; + +export function ThemeProvider({ + children, +}: { + children: React.ReactNode; +}): React.ReactElement { + const [theme, setTheme] = useState("light"); + const toggleTheme = () => { + setTheme(theme === "light" ? "dark" : "light"); + }; + + useEffect(() => { + document.body.classList.remove("light", "dark"); + document.body.classList.add(theme); + }, [theme]); + + return ( + + {children} + + ); +} From bf94c65581e9939a11061f1c36e76cdb73b74799 Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Tue, 5 Aug 2025 02:10:43 +0300 Subject: [PATCH 3/9] feat: redux add and remove item --- package-lock.json | 99 +++++++++++++++++++++++++ package.json | 2 + src/About.tsx | 2 +- src/App.css | 32 +++++++- src/App.tsx | 28 ++++--- src/{ => Header}/Header.css | 0 src/{ => Header}/Header.tsx | 11 ++- src/Input.tsx | 2 +- src/MainPage.tsx | 26 +++++-- src/PlanetMiniCard.tsx | 48 +++++++++--- src/Planets.tsx | 2 + src/SelectedPlanets/SelectedPlanets.css | 23 ++++++ src/SelectedPlanets/SelectedPlanets.tsx | 22 ++++++ src/hooks/hooks.ts | 4 + src/store/selectedItemsSlice.ts | 32 ++++++++ src/store/store.ts | 11 +++ 16 files changed, 310 insertions(+), 34 deletions(-) rename src/{ => Header}/Header.css (100%) rename src/{ => Header}/Header.tsx (71%) create mode 100644 src/SelectedPlanets/SelectedPlanets.css create mode 100644 src/SelectedPlanets/SelectedPlanets.tsx create mode 100644 src/hooks/hooks.ts create mode 100644 src/store/selectedItemsSlice.ts create mode 100644 src/store/store.ts diff --git a/package-lock.json b/package-lock.json index 49b8a8e..60709c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "rs-react-app", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.8.2", "@types/react-router-dom": "^5.3.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-redux": "^9.2.0", "react-router": "^7.7.1", "react-router-dom": "^7.7.1" }, @@ -1325,6 +1327,31 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -1591,6 +1618,16 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -1816,6 +1853,11 @@ "@testing-library/user-event": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.36.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", @@ -4039,6 +4081,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5359,6 +5410,28 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5417,6 +5490,19 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5459,6 +5545,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -6476,6 +6567,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", diff --git a/package.json b/package.json index 4f8c9a3..ba9326a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "coverage": "vitest run --coverage" }, "dependencies": { + "@reduxjs/toolkit": "^2.8.2", "@types/react-router-dom": "^5.3.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-redux": "^9.2.0", "react-router": "^7.7.1", "react-router-dom": "^7.7.1" }, diff --git a/src/About.tsx b/src/About.tsx index ccc1750..59b37e4 100644 --- a/src/About.tsx +++ b/src/About.tsx @@ -1,5 +1,5 @@ import React from "react"; -import Header from "./Header"; +import Header from "./Header/Header"; function About(): React.ReactElement { return (
diff --git a/src/App.css b/src/App.css index 210c7c8..a936122 100644 --- a/src/App.css +++ b/src/App.css @@ -43,7 +43,7 @@ body.dark { margin-bottom: 30px; } -input { +.main-input { flex-grow: 1; padding: 12px 15px; border: 2px solid #444; @@ -91,11 +91,39 @@ input { gap: 10px; } +.planet-checkbox { + width: 20px; + height: 20px; + + appearance: none; + outline: none; + cursor: pointer; + + border: 2px solid #000; + background-color: #fff; + border-radius: 4px; + + position: relative; + transition: all 0.2s ease; +} + +.planet-checkbox:checked { + background-color: #ffd700; + border-color: #000; +} + +.planet-checkbox:hover { + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.3); +} + .planet-card { + display: flex; + justify-content: space-between; background-color: #333; border-radius: 8px; padding: 10px 20px 10px 20px; transition: transform 0.3s; + cursor: pointer; } .planet-details { @@ -222,8 +250,8 @@ input { transform: rotate(360deg); } } + .error-test-button { - margin: 20px auto; padding: 10px 20px; background-color: #ffe81f; color: rgb(0, 0, 0); diff --git a/src/App.tsx b/src/App.tsx index 21b157e..e030b6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,22 +6,26 @@ import ErrorBoundary from "./ErrorBoundary"; import { BrowserRouter } from "react-router"; import PlanetCard from "./PlanetCard"; import { ThemeProvider } from "./ThemeProvider"; +import { Provider } from "react-redux"; +import { store } from "./store/store"; function App() { return ( - - - - } /> - }> - } /> - - } /> - } /> - - - + + + + + } /> + }> + } /> + + } /> + } /> + + + + ); } diff --git a/src/Header.css b/src/Header/Header.css similarity index 100% rename from src/Header.css rename to src/Header/Header.css diff --git a/src/Header.tsx b/src/Header/Header.tsx similarity index 71% rename from src/Header.tsx rename to src/Header/Header.tsx index 92cb43b..dc1019d 100644 --- a/src/Header.tsx +++ b/src/Header/Header.tsx @@ -1,8 +1,14 @@ import { NavLink } from "react-router-dom"; import "./Header.css"; -import { ThemeContext } from "./ThemeContext"; +import { ThemeContext } from "../ThemeContext"; import { useContext } from "react"; -function Header() { +import ErrorButton from "../ErrorButton"; + +interface HeaderProps { + handleError: () => void; +} + +function Header({ handleError }: HeaderProps) { const { theme, toggleTheme } = useContext(ThemeContext) || {}; return (
@@ -17,6 +23,7 @@ function Header() { +
); } diff --git a/src/Input.tsx b/src/Input.tsx index d18c6be..5ca588c 100644 --- a/src/Input.tsx +++ b/src/Input.tsx @@ -10,7 +10,7 @@ function Input({ value, onChange }: InputProps): React.ReactElement { const { theme } = useContext(ThemeContext) || {}; return ( { + if (selectedItems.some((selected) => selected.uid === item.uid)) { + dispatch(removeItem(item.uid)); + } else { + dispatch(addItem(item)); + } + }; + const getPlanets = useCallback(async () => { let errorMessage = "Unknown error"; setIsLoading(true); @@ -103,7 +117,7 @@ function MainPage(): React.ReactElement { return (
-
+

Star Wars Planets

{error ?
{error}
: null} @@ -117,6 +131,8 @@ function MainPage(): React.ReactElement { searchPlanets={searchPlanets} isLoading={isLoading} inputValue={inputValue} + onItemSelect={toggleItemSelection} + selectedItems={selectedItems} />
@@ -128,7 +144,7 @@ function MainPage(): React.ReactElement { searchQuery={searchQuery} > )} - + {!isLoading && }
); } diff --git a/src/PlanetMiniCard.tsx b/src/PlanetMiniCard.tsx index ce45cff..77c4255 100644 --- a/src/PlanetMiniCard.tsx +++ b/src/PlanetMiniCard.tsx @@ -2,6 +2,10 @@ import React from "react"; import type { PlanetsListItem } from "./types"; import { useNavigate, useParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { addItem, removeItem, stateItems } from "./store/selectedItemsSlice"; +import { useAppSelector } from "./hooks/hooks.ts"; + interface PlanetMiniCardProps { planet: PlanetsListItem; } @@ -10,21 +14,43 @@ function PlanetMiniCard({ planet }: PlanetMiniCardProps): React.ReactElement { const { pageNumber } = useParams(); const [searchParams] = useSearchParams(); const searchQuery = searchParams.get("search") || ""; + const navigate = useNavigate(); + + const dispatch = useDispatch(); + const selectedItems = useAppSelector(stateItems); + const id = planet.url.split("/").pop(); + + const isSelected = selectedItems.some((item) => item.uid === planet.uid); + + const handleCheckboxChange = (e: React.ChangeEvent) => { + e.stopPropagation(); + if (isSelected) { + dispatch(removeItem(planet.uid)); + } else { + dispatch(addItem(planet)); + } + }; + + const handleCardClick = () => { + if (!pageNumber) { + navigate(`/list/1/${id}?search=${searchQuery}`); + } else { + navigate(`/list/${pageNumber}/${id}?search=${searchQuery}`); + } + }; + return ( -
{ - if (!pageNumber) { - navigate(`/list/1/${id}?search=${searchQuery}`); - } else { - navigate(`/list/${pageNumber}/${id}?search=${searchQuery}`); - } - }} - > +

{planet.name}

+ e.stopPropagation()} + className="planet-checkbox" + />
); } diff --git a/src/Planets.tsx b/src/Planets.tsx index 6201c7f..742ba99 100644 --- a/src/Planets.tsx +++ b/src/Planets.tsx @@ -6,6 +6,8 @@ interface PlanetsProps { searchPlanets: PlanetsListItem[]; isLoading: boolean; inputValue: string; + onItemSelect: (item: PlanetsListItem) => void; + selectedItems: PlanetsListItem[]; } export default function Planets({ diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css new file mode 100644 index 0000000..3dd87d6 --- /dev/null +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -0,0 +1,23 @@ +.selected-planets { + position: fixed; + bottom: -1000px; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 1rem; + z-index: 1000; + max-height: 50vh; + transition: bottom 0.3s ease; +} + +.selected-planets-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1%; +} + +.selected-planets.visible { + bottom: 0px; +} \ No newline at end of file diff --git a/src/SelectedPlanets/SelectedPlanets.tsx b/src/SelectedPlanets/SelectedPlanets.tsx new file mode 100644 index 0000000..2144235 --- /dev/null +++ b/src/SelectedPlanets/SelectedPlanets.tsx @@ -0,0 +1,22 @@ +import type { PlanetsListItem } from "../types.ts"; +import { useAppSelector } from "../hooks/hooks.ts"; +import { stateItems } from "../store/selectedItemsSlice.ts"; +import "./SelectedPlanets.css"; + +export default function SelectedPlanets() { + const selectedItems = useAppSelector(stateItems); + const hasItems = selectedItems.length > 0; + + return ( +
+

Selected Planets

+
+ {selectedItems.map((planet: PlanetsListItem) => ( +
+ {planet.name} +
+ ))} +
+
+ ); +} diff --git a/src/hooks/hooks.ts b/src/hooks/hooks.ts new file mode 100644 index 0000000..5908179 --- /dev/null +++ b/src/hooks/hooks.ts @@ -0,0 +1,4 @@ +import { useSelector } from "react-redux"; +import type { RootState } from "../store/store"; + +export const useAppSelector = useSelector.withTypes(); diff --git a/src/store/selectedItemsSlice.ts b/src/store/selectedItemsSlice.ts new file mode 100644 index 0000000..c86b894 --- /dev/null +++ b/src/store/selectedItemsSlice.ts @@ -0,0 +1,32 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { PlanetsListItem } from "../types"; +import type { RootState } from "../store/store"; + +interface SelectedItemsState { + items: PlanetsListItem[]; +} + +const initialState: SelectedItemsState = { + items: [], +}; + +export const selectedItemsSlice = createSlice({ + name: "selectedItems", + initialState, + reducers: { + addItem: (state, action: PayloadAction) => { + if (!state.items.find((item) => item.uid === action.payload.uid)) { + state.items.push(action.payload); + } + }, + removeItem: (state, action: PayloadAction) => { + state.items = state.items.filter((item) => item.uid !== action.payload); + }, + }, +}); + +export const stateItems = (state: RootState) => state.selectedItems.items; + +export const { addItem, removeItem } = selectedItemsSlice.actions; +export default selectedItemsSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..eb02d11 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from "@reduxjs/toolkit"; +import selectedItemsReducer from "./selectedItemsSlice"; + +export const store = configureStore({ + reducer: { + selectedItems: selectedItemsReducer, + }, +}); +export type AppStore = typeof store; +export type RootState = ReturnType; +export type AppDispatch = AppStore["dispatch"]; From 78e046e4cfbccc98e706a0d909f9f188bdace5d7 Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Tue, 5 Aug 2025 02:26:57 +0300 Subject: [PATCH 4/9] feat: add redux and fix test --- src/SelectedPlanets/SelectedPlanets.css | 2 +- src/__tests__/App_test1.test.tsx | 10 ++-- .../CardList_Component_Tests.test.tsx | 26 +++++---- src/__tests__/Error.test.tsx | 26 +++++---- .../Search_\320\241omponent_Tests.test.tsx" | 54 ++++++++++++------- 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css index 3dd87d6..191a860 100644 --- a/src/SelectedPlanets/SelectedPlanets.css +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -20,4 +20,4 @@ .selected-planets.visible { bottom: 0px; -} \ No newline at end of file +} diff --git a/src/__tests__/App_test1.test.tsx b/src/__tests__/App_test1.test.tsx index 6080f06..2e42b31 100644 --- a/src/__tests__/App_test1.test.tsx +++ b/src/__tests__/App_test1.test.tsx @@ -1,13 +1,17 @@ import { screen, render } from "@testing-library/react"; import MainPage from "../MainPage"; import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; describe("App tests render", () => { test("should render the title", () => { render( - - - , + + + + + , ); expect( diff --git a/src/__tests__/CardList_Component_Tests.test.tsx b/src/__tests__/CardList_Component_Tests.test.tsx index c702f61..69fb0a4 100644 --- a/src/__tests__/CardList_Component_Tests.test.tsx +++ b/src/__tests__/CardList_Component_Tests.test.tsx @@ -5,6 +5,8 @@ import MainPage from "../MainPage"; import { PlanetApi } from "../PlanetFetch"; import { BrowserRouter as Router } from "react-router-dom"; import type { PlanetsListItem } from "../types"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; vi.mock("../PlanetFetch", () => ({ PlanetApi: { @@ -37,9 +39,11 @@ describe("Planet Cards Rendering", () => { const user = userEvent.setup(); vi.mocked(PlanetApi.fetchPlanets).mockResolvedValue(mockPlanet); render( - - - , + + + + + , ); const inputElement = screen.getByRole("textbox"); @@ -57,9 +61,11 @@ describe("Planet Cards Rendering", () => { test("should render all planet cards", async () => { vi.mocked(PlanetApi.fetchPlanets).mockResolvedValue(mockPlanet); render( - - - , + + + + + , ); expect(await screen.findByRole("article")).toBeInTheDocument(); expect(screen.getByText(mockPlanet.planets[0].name)).toBeInTheDocument(); @@ -69,9 +75,11 @@ describe("Planet Cards Rendering", () => { new Error("Error in request"), ); render( - - - , + + + + + , ); const error = await screen.findByTestId("error-message"); diff --git a/src/__tests__/Error.test.tsx b/src/__tests__/Error.test.tsx index 07a2204..8ce1478 100644 --- a/src/__tests__/Error.test.tsx +++ b/src/__tests__/Error.test.tsx @@ -4,17 +4,21 @@ import ErrorBoundary from "../ErrorBoundary"; import { describe, it, expect, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; describe("Error component", () => { it("should throw error", async () => { const ErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); render( - - - - - , + + + + + + + , ); const errorButton = screen.getByRole("button", { name: /Error/ }); @@ -30,11 +34,13 @@ describe("Error component", () => { it("should click reboot button", async () => { render( - - - - - , + + + + + + + , ); const errorButton = screen.getByRole("button", { name: /Error/ }); diff --git "a/src/__tests__/Search_\320\241omponent_Tests.test.tsx" "b/src/__tests__/Search_\320\241omponent_Tests.test.tsx" index c069772..e04e9c8 100644 --- "a/src/__tests__/Search_\320\241omponent_Tests.test.tsx" +++ "b/src/__tests__/Search_\320\241omponent_Tests.test.tsx" @@ -3,6 +3,8 @@ import { render, screen } from "@testing-library/react"; import MainPage from "../MainPage"; import userEvent from "@testing-library/user-event"; import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; describe("Search Component Tests", () => { const localStorageMock = { @@ -16,9 +18,11 @@ describe("Search Component Tests", () => { test("should render Input and Button elements", () => { render( - - - , + + + + + , ); const inputElement = screen.getByRole("textbox"); @@ -38,9 +42,11 @@ describe("Search Component Tests", () => { } }); render( - - - , + + + + + , ); expect(screen.getByRole("textbox")).toHaveValue(testQuery); expect(localStorageMock.getItem).toHaveBeenCalledWith("starWarsQuery"); @@ -51,9 +57,12 @@ describe("Search Component Tests", () => { return null; }); render( - - - , + + + + + , + , ); expect(screen.getByRole("textbox")).toHaveValue(""); expect(localStorageMock.getItem).toHaveBeenCalledWith("starWarsQuery"); @@ -62,9 +71,12 @@ describe("Search Component Tests", () => { test("should update input value when typing", async () => { const user = userEvent.setup(); render( - - - , + + + + + , + , ); const inputElement = screen.getByRole("textbox"); @@ -77,9 +89,12 @@ describe("Search Component Tests", () => { const user = userEvent.setup(); render( - - - , + + + + + , + , ); const inputElement = screen.getByRole("textbox"); @@ -98,9 +113,12 @@ describe("Search Component Tests", () => { const user = userEvent.setup(); render( - - - , + + + + + , + , ); const inputElement = screen.getByRole("textbox"); From b424fd14ad9040edb8d9afd02d4b0488bf8b7d4c Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Wed, 6 Aug 2025 01:36:12 +0300 Subject: [PATCH 5/9] fix: show selected plantes to selected quantity --- src/SelectedPlanets/SelectedPlanets.css | 7 ------- src/SelectedPlanets/SelectedPlanets.tsx | 10 +--------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css index 191a860..389141b 100644 --- a/src/SelectedPlanets/SelectedPlanets.css +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -11,13 +11,6 @@ transition: bottom 0.3s ease; } -.selected-planets-list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 1%; -} - .selected-planets.visible { bottom: 0px; } diff --git a/src/SelectedPlanets/SelectedPlanets.tsx b/src/SelectedPlanets/SelectedPlanets.tsx index 2144235..691071a 100644 --- a/src/SelectedPlanets/SelectedPlanets.tsx +++ b/src/SelectedPlanets/SelectedPlanets.tsx @@ -1,4 +1,3 @@ -import type { PlanetsListItem } from "../types.ts"; import { useAppSelector } from "../hooks/hooks.ts"; import { stateItems } from "../store/selectedItemsSlice.ts"; import "./SelectedPlanets.css"; @@ -9,14 +8,7 @@ export default function SelectedPlanets() { return (
-

Selected Planets

-
- {selectedItems.map((planet: PlanetsListItem) => ( -
- {planet.name} -
- ))} -
+

Selected Planets: {selectedItems.length}

); } From da5852db778315cd6654895d2227157d81eac615 Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Wed, 6 Aug 2025 04:05:30 +0300 Subject: [PATCH 6/9] feat: add download elements in CSV --- src/SelectedPlanets/SelectedPlanets.css | 24 ++++++++++++++++ src/SelectedPlanets/SelectedPlanets.tsx | 37 +++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css index 389141b..55187e2 100644 --- a/src/SelectedPlanets/SelectedPlanets.css +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -14,3 +14,27 @@ .selected-planets.visible { bottom: 0px; } + +.download_button{ + align-self: flex-end; + background-color: #000000; + color: #ffe81f; + border: 2px solid #ffe81f; + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + cursor: pointer; + width: 30%; + text-transform: uppercase; + transition: all 0.3s ease; +} + +.download_button:hover { + background-color: #ffe81f; + color: #000000; + box-shadow: 0 0 10px #ffe81f; +} + +.download_button:active { + transform: scale(0.95); +} \ No newline at end of file diff --git a/src/SelectedPlanets/SelectedPlanets.tsx b/src/SelectedPlanets/SelectedPlanets.tsx index 691071a..66131a8 100644 --- a/src/SelectedPlanets/SelectedPlanets.tsx +++ b/src/SelectedPlanets/SelectedPlanets.tsx @@ -1,14 +1,47 @@ import { useAppSelector } from "../hooks/hooks.ts"; import { stateItems } from "../store/selectedItemsSlice.ts"; +import { useRef, useState } from "react"; import "./SelectedPlanets.css"; export default function SelectedPlanets() { const selectedItems = useAppSelector(stateItems); - const hasItems = selectedItems.length > 0; + const downloadRef = useRef(null); + const [downloadUrl, setDownloadUrl] = useState(""); + const [filename, setFilename] = useState(""); + + function download() { + const name_column = "id,name,url \r\n"; + const download_data = selectedItems + .map((items, id) => id + 1 + "," + items.name + "," + items.url) + .join("\r\n"); + const csvdata = name_column + download_data; + const csvBlob = new Blob([csvdata], { type: "text/csv" }); + const url = URL.createObjectURL(csvBlob); + setDownloadUrl(url); + setFilename(`${selectedItems.length}_items.csv`); + setTimeout(() => { + if (downloadRef.current) { + downloadRef.current.click(); + URL.revokeObjectURL(url); + setDownloadUrl(""); + } + }, 0); + } return ( -
+

Selected Planets: {selectedItems.length}

+ +
); } From 5027fa6696e8dd2b42d941a52c1ec958fee34f92 Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Thu, 7 Aug 2025 04:09:02 +0300 Subject: [PATCH 7/9] feat: button clear all --- src/SelectedPlanets/SelectedPlanets.css | 24 ++++++++++++++++++++++++ src/SelectedPlanets/SelectedPlanets.tsx | 12 +++++++++++- src/store/selectedItemsSlice.ts | 6 +++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css index 55187e2..21738d0 100644 --- a/src/SelectedPlanets/SelectedPlanets.css +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -37,4 +37,28 @@ .download_button:active { transform: scale(0.95); +} + +.clear_button{ + align-self: flex-end; + background-color: #000000; + color: #ffe81f; + border: 2px solid #ffe81f; + padding: 8px 16px; + font-weight: bold; + border-radius: 4px; + cursor: pointer; + width: 30%; + text-transform: uppercase; + transition: all 0.3s ease; +} + +.clear_button:hover { + background-color: #ffe81f; + color: #000000; + box-shadow: 0 0 10px #ffe81f; +} + +.clear_button:active { + transform: scale(0.95); } \ No newline at end of file diff --git a/src/SelectedPlanets/SelectedPlanets.tsx b/src/SelectedPlanets/SelectedPlanets.tsx index 66131a8..86e7b25 100644 --- a/src/SelectedPlanets/SelectedPlanets.tsx +++ b/src/SelectedPlanets/SelectedPlanets.tsx @@ -1,6 +1,7 @@ import { useAppSelector } from "../hooks/hooks.ts"; -import { stateItems } from "../store/selectedItemsSlice.ts"; +import { stateItems, clearAllItems } from "../store/selectedItemsSlice.ts"; import { useRef, useState } from "react"; +import { useDispatch } from "react-redux"; import "./SelectedPlanets.css"; export default function SelectedPlanets() { @@ -9,6 +10,12 @@ export default function SelectedPlanets() { const [downloadUrl, setDownloadUrl] = useState(""); const [filename, setFilename] = useState(""); + const dispatch = useDispatch(); + + const handleClearAll = () => { + dispatch(clearAllItems()); + }; + function download() { const name_column = "id,name,url \r\n"; const download_data = selectedItems @@ -36,6 +43,9 @@ export default function SelectedPlanets() { + ) => { state.items = state.items.filter((item) => item.uid !== action.payload); }, + clearAllItems: (state) => { + state.items = []; + }, }, }); export const stateItems = (state: RootState) => state.selectedItems.items; -export const { addItem, removeItem } = selectedItemsSlice.actions; +export const { addItem, removeItem, clearAllItems } = + selectedItemsSlice.actions; export default selectedItemsSlice.reducer; From 27e5d19d13d71f9fb08cc05f46fbdc5fcb3a6cee Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Fri, 8 Aug 2025 04:09:51 +0300 Subject: [PATCH 8/9] fix: router and header button error --- src/About.tsx | 2 -- src/App.tsx | 13 ++++++++----- src/Header/Header.tsx | 6 +----- src/MainPage.tsx | 17 ++--------------- src/SelectedPlanets/SelectedPlanets.css | 6 +++--- src/Wrapper.tsx | 25 +++++++++++++++++++++++++ src/__tests__/App_test1.test.tsx | 2 ++ src/__tests__/Error.test.tsx | 3 +++ 8 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 src/Wrapper.tsx diff --git a/src/About.tsx b/src/About.tsx index 59b37e4..65c3a8d 100644 --- a/src/About.tsx +++ b/src/About.tsx @@ -1,9 +1,7 @@ import React from "react"; -import Header from "./Header/Header"; function About(): React.ReactElement { return (
-

Star Wars Planets App

The author of the application is Pavel Kozin

diff --git a/src/App.tsx b/src/App.tsx index e030b6f..def1f3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import PlanetCard from "./PlanetCard"; import { ThemeProvider } from "./ThemeProvider"; import { Provider } from "react-redux"; import { store } from "./store/store"; +import Wrapper from "./Wrapper"; function App() { return ( @@ -16,12 +17,14 @@ function App() { - } /> - }> - } /> + }> + } /> + } /> + }> + } /> + + } /> - } /> - } /> diff --git a/src/Header/Header.tsx b/src/Header/Header.tsx index dc1019d..cadf6c1 100644 --- a/src/Header/Header.tsx +++ b/src/Header/Header.tsx @@ -4,11 +4,7 @@ import { ThemeContext } from "../ThemeContext"; import { useContext } from "react"; import ErrorButton from "../ErrorButton"; -interface HeaderProps { - handleError: () => void; -} - -function Header({ handleError }: HeaderProps) { +function Header({ handleError }: { handleError: () => void }) { const { theme, toggleTheme } = useContext(ThemeContext) || {}; return (
diff --git a/src/MainPage.tsx b/src/MainPage.tsx index 2eba795..cd398e8 100644 --- a/src/MainPage.tsx +++ b/src/MainPage.tsx @@ -12,7 +12,6 @@ import { useNavigate, } from "react-router-dom"; import "./App.css"; -import Header from "./Header/Header"; import { PlanetApi } from "./PlanetFetch"; import Planets from "./Planets"; import Button from "./Button"; @@ -97,11 +96,6 @@ function MainPage(): React.ReactElement { }, [getPlanets, searchQuery]); const [error, setError] = useState(null); - const [errorBoolean, setErrorBoolean] = useState(false); - - function triggerError() { - setErrorBoolean(true); - } function handleSearch() { navigate(`/list/1?search=${encodeURIComponent(inputValue)}`); @@ -111,17 +105,10 @@ function MainPage(): React.ReactElement { setInputValue(e.target.value); } - if (errorBoolean) { - throw new Error("Test error"); - } - return ( -
-
+ <>

Star Wars Planets

- {error ?
{error}
: null} -
@@ -145,7 +132,7 @@ function MainPage(): React.ReactElement { > )} {!isLoading && } -
+ ); } diff --git a/src/SelectedPlanets/SelectedPlanets.css b/src/SelectedPlanets/SelectedPlanets.css index 21738d0..bd93acb 100644 --- a/src/SelectedPlanets/SelectedPlanets.css +++ b/src/SelectedPlanets/SelectedPlanets.css @@ -15,7 +15,7 @@ bottom: 0px; } -.download_button{ +.download_button { align-self: flex-end; background-color: #000000; color: #ffe81f; @@ -39,7 +39,7 @@ transform: scale(0.95); } -.clear_button{ +.clear_button { align-self: flex-end; background-color: #000000; color: #ffe81f; @@ -61,4 +61,4 @@ .clear_button:active { transform: scale(0.95); -} \ No newline at end of file +} diff --git a/src/Wrapper.tsx b/src/Wrapper.tsx new file mode 100644 index 0000000..b81384b --- /dev/null +++ b/src/Wrapper.tsx @@ -0,0 +1,25 @@ +import { Outlet } from "react-router-dom"; +import Header from "./Header/Header"; +import { useContext, useState } from "react"; +import { ThemeContext } from "./ThemeContext"; + +function Wrapper() { + const { theme } = useContext(ThemeContext) || {}; + const [errorBoolean, setErrorBoolean] = useState(false); + + function triggerError() { + setErrorBoolean(true); + } + if (errorBoolean) { + throw new Error("Test error"); + } + + return ( +
+
+ +
+ ); +} + +export default Wrapper; diff --git a/src/__tests__/App_test1.test.tsx b/src/__tests__/App_test1.test.tsx index 2e42b31..6a72493 100644 --- a/src/__tests__/App_test1.test.tsx +++ b/src/__tests__/App_test1.test.tsx @@ -3,6 +3,7 @@ import MainPage from "../MainPage"; import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; import { store } from "../store/store"; +import Wrapper from "../Wrapper"; describe("App tests render", () => { test("should render the title", () => { @@ -10,6 +11,7 @@ describe("App tests render", () => { + , ); diff --git a/src/__tests__/Error.test.tsx b/src/__tests__/Error.test.tsx index 8ce1478..c8e6dbc 100644 --- a/src/__tests__/Error.test.tsx +++ b/src/__tests__/Error.test.tsx @@ -6,6 +6,7 @@ import userEvent from "@testing-library/user-event"; import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; import { store } from "../store/store"; +import Wrapper from "../Wrapper"; describe("Error component", () => { it("should throw error", async () => { @@ -15,6 +16,7 @@ describe("Error component", () => { + @@ -37,6 +39,7 @@ describe("Error component", () => { + From fbc65537fcfa9c57a6ab6349deaa959cc3fa69aa Mon Sep 17 00:00:00 2001 From: PAVEL KOZIN Date: Fri, 8 Aug 2025 05:57:42 +0300 Subject: [PATCH 9/9] feat: fix and add test --- src/__tests__/About.test.tsx | 22 ++++++++ src/__tests__/App.test.tsx | 20 ++++++++ src/__tests__/Card.test.tsx | 51 +++++++++++++++++++ ...onent_Tests.test.tsx => CardList.test.tsx} | 0 .../{App_test1.test.tsx => Header.test.tsx} | 0 src/__tests__/Page404.test.tsx | 24 +++++++++ .../__tests__/Search.test.tsx | 0 7 files changed, 117 insertions(+) create mode 100644 src/__tests__/About.test.tsx create mode 100644 src/__tests__/App.test.tsx create mode 100644 src/__tests__/Card.test.tsx rename src/__tests__/{CardList_Component_Tests.test.tsx => CardList.test.tsx} (100%) rename src/__tests__/{App_test1.test.tsx => Header.test.tsx} (100%) create mode 100644 src/__tests__/Page404.test.tsx rename "src/__tests__/Search_\320\241omponent_Tests.test.tsx" => src/__tests__/Search.test.tsx (100%) diff --git a/src/__tests__/About.test.tsx b/src/__tests__/About.test.tsx new file mode 100644 index 0000000..ea573c3 --- /dev/null +++ b/src/__tests__/About.test.tsx @@ -0,0 +1,22 @@ +import { screen, render } from "@testing-library/react"; +import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; +import About from "../About"; + +describe("App tests render", () => { + test("should render about", () => { + render( + + + + + , + ); + + expect(screen.getByRole("paragraph")).toHaveTextContent( + "The author of the application is Pavel Kozin", + ); + expect(screen.getByText("RS School")).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx new file mode 100644 index 0000000..7d3c37d --- /dev/null +++ b/src/__tests__/App.test.tsx @@ -0,0 +1,20 @@ +import { screen, render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { BrowserRouter } from "react-router-dom"; +import { store } from "../store/store"; +import App from "../App"; + +describe("App tests render", () => { + test("should render app", () => { + render( + + + + + , + ); + expect(screen.getByTestId("app")).toBeInTheDocument(); + expect(screen.getByRole("navigation")).toBeInTheDocument(); + expect(screen.getByText("Star Wars Planets")).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/Card.test.tsx b/src/__tests__/Card.test.tsx new file mode 100644 index 0000000..b4699ab --- /dev/null +++ b/src/__tests__/Card.test.tsx @@ -0,0 +1,51 @@ +import { describe, test, expect, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import PlanetCard from "../PlanetCard"; +import { PlanetApi } from "../PlanetFetch"; +import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; +import type { Params } from "react-router-dom"; + +vi.mock("../PlanetFetch", () => ({ + PlanetApi: { + fetchPlanetDetail: vi.fn(), + }, +})); + +vi.mock("react-router-dom", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + useParams: (): Readonly> => ({ planetId: "2" }), + }; +}); + +describe("Planet Cards Rendering", () => { + test("should render 1 planet cards", async () => { + const mockPlanetData = { + name: "Alderaan", + diameter: "12500", + climate: "temperate", + population: "2000000000", + rotation_period: "24", + orbital_period: "364", + gravity: "1 standard", + terrain: "grasslands, mountains", + }; + + vi.mocked(PlanetApi.fetchPlanetDetail).mockResolvedValue(mockPlanetData); + render( + + + + + , + ); + await waitFor(() => { + expect( + screen.getByRole("heading", { name: /Alderaan/ }), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__tests__/CardList_Component_Tests.test.tsx b/src/__tests__/CardList.test.tsx similarity index 100% rename from src/__tests__/CardList_Component_Tests.test.tsx rename to src/__tests__/CardList.test.tsx diff --git a/src/__tests__/App_test1.test.tsx b/src/__tests__/Header.test.tsx similarity index 100% rename from src/__tests__/App_test1.test.tsx rename to src/__tests__/Header.test.tsx diff --git a/src/__tests__/Page404.test.tsx b/src/__tests__/Page404.test.tsx new file mode 100644 index 0000000..1127124 --- /dev/null +++ b/src/__tests__/Page404.test.tsx @@ -0,0 +1,24 @@ +import { screen, render } from "@testing-library/react"; +import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "../store/store"; +import Page404 from "../Page404"; + +describe("App tests render", () => { + test("should render about", () => { + render( + + + + + , + ); + + expect( + screen.getByRole("heading", { + level: 1, + }), + ).toHaveTextContent("404"); + expect(screen.getByRole("link", { name: /Вернуться на главную/ })); + }); +}); diff --git "a/src/__tests__/Search_\320\241omponent_Tests.test.tsx" b/src/__tests__/Search.test.tsx similarity index 100% rename from "src/__tests__/Search_\320\241omponent_Tests.test.tsx" rename to src/__tests__/Search.test.tsx