diff --git a/cypress/integration/home/error-fallback.spec.js b/cypress/integration/home/error-fallback.spec.js new file mode 100644 index 0000000..92bba52 --- /dev/null +++ b/cypress/integration/home/error-fallback.spec.js @@ -0,0 +1,17 @@ +import { streamsRegex } from "../../consts/urlRegexes"; + +describe("Home > Error Fallback", () => { + it("should throw error fallback when request fails", () => { + cy.intercept(streamsRegex, { + statusCode: 400, + }); + + cy.visit(Cypress.env("hostUrl")); + + cy.get(".chakra-text").eq(2).should("have.text", "Oops! Algo de errado não está certo"); + cy.get(".chakra-text") + .eq(3) + .should("have.text", "Não conseguimos carregar o que você estava procurando 😔"); + cy.get("button").should("have.text", "Tente novamente"); + }); +}); diff --git a/package.json b/package.json index c19e178..295c88e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-apexcharts": "^1.3.9", "react-cookie": "^4.1.1", "react-dom": "^17.0.2", + "react-error-boundary": "^3.1.4", "react-ga": "^3.3.0", "react-icons": "^4.3.1", "react-router-dom": "^6.2.1", @@ -87,9 +88,8 @@ "lint-staged": ">=12", "prettier": "^2.5.1" }, - "lint-staged": { - "src/**/*": [ + "src/**/!*.css": [ "yarn lint --fix" ] }, diff --git a/src/App.css b/src/App.css index 393aaea..572071c 100644 --- a/src/App.css +++ b/src/App.css @@ -3,7 +3,6 @@ body { background-color: #33374D !important; height: 100%; - padding-bottom: 200px; } .logo-title { diff --git a/src/App.tsx b/src/App.tsx index f6184f0..ea8a149 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,18 @@ import React, { useEffect } from "react"; +import ReactGA from "react-ga"; import { Routes, Route } from "react-router-dom"; -import "./App.css"; +import { ErrorBoundary } from "react-error-boundary"; -import ToPage from "./pages/to/ToPage"; +import "./App.css"; import Home from "./pages/Home"; import About from "./pages/About"; import Stats from "./pages/Estatisticas"; import Supporters from "./pages/Supporters"; +import ToPage from "./pages/to/ToPage"; -import ReactGA from "react-ga"; +import LandingLayout from "./components/layouts/LandingLayout"; +import ErrorFallback from "./components/sections/ErrorFallback"; function App() { useEffect(() => { @@ -17,15 +20,19 @@ function App() { }); return ( - - } /> - } /> - } /> - } /> - {/* } /> + + + + } /> + } /> + } /> + } /> + {/* } /> } /> */} - } /> - + } /> + + + ); } diff --git a/src/components/layouts/LandingLayout.tsx b/src/components/layouts/LandingLayout.tsx index 1595571..63da707 100644 --- a/src/components/layouts/LandingLayout.tsx +++ b/src/components/layouts/LandingLayout.tsx @@ -9,10 +9,10 @@ type Props = { export default function LandingLayout({ children }: Props) { return ( - +
- - + + {children} diff --git a/src/components/sections/ErrorFallback.tsx b/src/components/sections/ErrorFallback.tsx new file mode 100644 index 0000000..6317b3f --- /dev/null +++ b/src/components/sections/ErrorFallback.tsx @@ -0,0 +1,25 @@ +import { Center, VStack, Text, Button, Box } from "@chakra-ui/react"; +import { RepeatIcon } from "@chakra-ui/icons"; +import { FallbackProps } from "react-error-boundary"; + +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { + return ( +
+ + + + Oops! Algo de errado não está certo + + + Não conseguimos carregar o que você estava procurando 😔 + + + + +
+ ); +}; + +export default ErrorFallback; diff --git a/src/hooks/useAxios.ts b/src/hooks/useAxios.ts index eae6da5..43b5502 100644 --- a/src/hooks/useAxios.ts +++ b/src/hooks/useAxios.ts @@ -8,15 +8,12 @@ const api = axios.create({ export function useAxios() { const apiGet = useCallback( - async ( - endpoint: string, - config?: AxiosRequestConfig, - ) => { - const { data } = await api.get< - Response, - AxiosResponse, - Data - >(endpoint, config); + async (endpoint: string, config?: AxiosRequestConfig) => { + const { data } = await api.get, Data>( + endpoint, + config, + ); + return data; }, [], diff --git a/src/pages/About.tsx b/src/pages/About.tsx index 112ce02..ef48e6f 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -13,11 +13,10 @@ import { Text, Wrap, } from "@chakra-ui/react"; -import LandingLayout from "../components/layouts/LandingLayout"; export default function About() { return ( - + <> Sobre Saiba mais sobre o projeto! @@ -133,6 +132,6 @@ export default function About() { - + ); } diff --git a/src/pages/Estatisticas.tsx b/src/pages/Estatisticas.tsx index 066a99f..da7e995 100644 --- a/src/pages/Estatisticas.tsx +++ b/src/pages/Estatisticas.tsx @@ -18,25 +18,31 @@ import { import { useAxios } from "../hooks/useAxios"; import { endpoints } from "../service/api"; import { Stats, StatsSummary, StatsSummaryDefault } from "../types"; - -import LandingLayout from "../components/layouts/LandingLayout"; +import { useErrorHandler } from "react-error-boundary"; export default function StatsPage() { const { apiGet } = useAxios(); + const handleError = useErrorHandler(); + const [isLoading, setIsLoading] = useState(false); const [stats, setStats] = useState([]); const [statsSummary, setStatsSummary] = useState(StatsSummaryDefault); const loadData = useCallback(async () => { - setIsLoading(true); + try { + setIsLoading(true); - const statsList = await apiGet(endpoints.stats.url); - const statsSummaryList = await apiGet(endpoints.stats_summary.url); + const statsList = await apiGet(endpoints.stats.url); + const statsSummaryList = await apiGet(endpoints.stats_summary.url); - setStats(statsList); - setStatsSummary(statsSummaryList); - setIsLoading(false); - }, [apiGet]); + setStats(statsList); + setStatsSummary(statsSummaryList); + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }, [apiGet, handleError]); useEffect(() => { loadData(); @@ -61,7 +67,7 @@ export default function StatsPage() { }; return ( - + <> Estatísticas Saiba mais sobre o projeto! @@ -109,6 +115,6 @@ export default function StatsPage() { )} - + ); } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index ef0c4ed..a9e337b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -19,7 +19,6 @@ import { import type { Channel, Tag } from "../types"; -import LandingLayout from "../components/layouts/LandingLayout"; import { SkeletonListCard } from "../components/sections/SkeletonListCard"; import { SkeletonListTags } from "../components/sections/SkeletonListTags"; import Card from "../components/ui/Card"; @@ -27,12 +26,14 @@ import Mosaic from "../components/sections/Mosaic"; import { useAxios } from "../hooks/useAxios"; import { endpoints } from "../service/api"; import { useSearchParams } from "react-router-dom"; +import { useErrorHandler } from "react-error-boundary"; export default function Home() { const REFRESH_TIME_IN_SECONDS = 120; const { apiGet } = useAxios(); const buttonSize = useBreakpointValue({ base: "sm", md: "md" }); const [isLargerThan1000px] = useMediaQuery("(min-width: 1000px)"); + const handleError = useErrorHandler(); const [isMosaicMode, setIsMosaicMode] = useState(false); const [channels, setChannels] = useState([]); @@ -49,20 +50,24 @@ export default function Home() { const isRefetching = isLoading && hasData; const loadData = useCallback(async () => { - setIsFetching(true); + try { + setIsFetching(true); - const [channelsList, tagsList, vodsList] = await Promise.all([ - apiGet(endpoints.channels.url), - apiGet(endpoints.tags.url), - apiGet(endpoints.vods.url), - ]); + const [channelsList, tagsList, vodsList] = await Promise.all([ + apiGet(endpoints.channels.url), + apiGet(endpoints.tags.url), + apiGet(endpoints.vods.url), + ]); - setChannels(channelsList); - setTags(tagsList); - setVods(vodsList); - - setIsFetching(false); - }, [apiGet]); + setChannels(channelsList); + setTags(tagsList); + setVods(vodsList); + } catch (error) { + handleError(error); + } finally { + setIsFetching(false); + } + }, [apiGet, handleError]); const handleShuffleClick = () => { const channel = channels[Math.floor(Math.random() * channels.length)]; @@ -102,18 +107,20 @@ export default function Home() { useEffect(() => { const tagNames = searchParams.get("tags"); - if (tagNames && tags.length) { + if (tagNames && tags?.length) { const tagsNamesArray = decodeURIComponent(tagNames).split(","); - const newSelectedTags = tagsNamesArray.map((tag) => tags.find((t) => t.name === tag)) as Tag[]; + const newSelectedTags = tagsNamesArray.map((tag) => + tags.find((t) => t.name === tag), + ) as Tag[]; setSelectedTags(newSelectedTags); } }, [searchParams, tags]); - const filterChannelsByTags = (channels: Channel[], selectedTags: Tag[]) => { + const filterChannelsByTags = (channels: Channel[] | undefined, selectedTags: Tag[]) => { if (selectedTags.length === 0) { return channels; } - const filteredChannels = channels.filter((channel) => { + const filteredChannels = channels?.filter((channel) => { return selectedTags.every((selectedTag) => channel.tags?.includes(selectedTag.id)); }); @@ -126,7 +133,7 @@ export default function Home() { ); return ( - + <> @@ -204,7 +211,7 @@ export default function Home() { ) : ( <> - {tags.map((tag) => { + {tags?.map((tag) => { const isTagSelected = selectedTags.some((t) => t.id === tag.id); return ( ) : ( - {filteredChannels.map((channel) => ( + {filteredChannels?.map((channel) => ( ) : ( - {vods.map((channel) => ( + {vods?.map((channel) => ( } - + ); } diff --git a/src/pages/Supporters.tsx b/src/pages/Supporters.tsx index 77a7349..cc2b172 100644 --- a/src/pages/Supporters.tsx +++ b/src/pages/Supporters.tsx @@ -1,40 +1,37 @@ import React, { useState, useEffect, useCallback } from "react"; import axios from "axios"; -import { - Box, - Center, - Heading, - Image, - Spinner, - Text, - VStack, - Wrap, -} from "@chakra-ui/react"; +import { Box, Center, Heading, Image, Spinner, Text, VStack, Wrap } from "@chakra-ui/react"; import type { Supporter } from "../types"; - -import LandingLayout from "../components/layouts/LandingLayout"; +import { useErrorHandler } from "react-error-boundary"; export default function Supporters() { + const handleError = useErrorHandler(); + const [supporters, setSupporters] = useState([]); const [isLoading, setIsLoading] = useState(false); const loadData = useCallback(async () => { - setIsLoading(true); + try { + setIsLoading(true); - const supportersUrl = process.env.REACT_APP_SUPPORTERS || ""; - const { data } = await axios.get(supportersUrl); + const supportersUrl = process.env.REACT_APP_SUPPORTERS || ""; + const { data } = await axios.get(supportersUrl); - setSupporters(data); - setIsLoading(false); - }, []); + setSupporters(data); + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }, [handleError]); useEffect(() => { loadData(); }, [loadData]); return ( - + <> Agradecimentos Saiba mais sobre o projeto! @@ -76,6 +73,6 @@ export default function Supporters() { ))} )} - + ); } diff --git a/yarn.lock b/yarn.lock index 5186aef..957d0e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9424,6 +9424,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.10: version "6.0.10" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"