From 976f5e9c5089fb81ccdb2bd71223b07e08a5f0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Tue, 22 Mar 2022 20:57:40 -0300 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=F0=9F=9A=80=20add=20error=20bounda?= =?UTF-8?q?ry=20to=20home=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- src/App.css | 1 - src/App.tsx | 36 +++++++++----- src/components/layouts/LandingLayout.tsx | 6 +-- src/components/sections/ErrorFallback.tsx | 18 +++++++ src/hooks/useAxios.ts | 15 +++--- src/pages/About.tsx | 5 +- src/pages/Home.tsx | 57 +++++++++++++---------- src/pages/Stats.tsx | 19 ++------ src/pages/Supporters.tsx | 17 ++----- yarn.lock | 7 +++ 11 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 src/components/sections/ErrorFallback.tsx diff --git a/package.json b/package.json index 20f9d83..f754388 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,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", @@ -84,9 +85,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 247ce10..1ddbbf0 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/Stats"; 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,24 @@ 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..1a541ef --- /dev/null +++ b/src/components/sections/ErrorFallback.tsx @@ -0,0 +1,18 @@ +import { Center, VStack, Text } from "@chakra-ui/react"; + +const ErrorFallback = () => { + 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/Home.tsx b/src/pages/Home.tsx index e7baedc..19ab04d 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,17 +26,19 @@ 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([]); - const [tags, setTags] = useState([]); - const [vods, setVods] = useState([]); + const [channels, setChannels] = useState([]); + const [tags, setTags] = useState([]); + const [vods, setVods] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefetching, setIsReFetching] = useState(false); const [selectedChannels, setSelectedChannels] = useState([]); @@ -45,18 +46,22 @@ export default function Home() { const [searchParams, setSearchParams] = useSearchParams(); const loadData = useCallback(async () => { - setIsLoading(true); + try { + setIsLoading(true); - const channelsList = await apiGet(endpoints.channels.url); - const tagsList = await apiGet(endpoints.tags.url); - const vodsList = await apiGet(endpoints.vods.url); + const channelsList = await apiGet(endpoints.channels.url); + const tagsList = await apiGet(endpoints.tags.url); + const vodsList = await apiGet(endpoints.vods.url); - setChannels(channelsList); - setTags(tagsList); - setVods(vodsList); - - setIsLoading(false); - }, [apiGet]); + setChannels(channelsList); + setTags(tagsList); + setVods(vodsList); + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }, [apiGet, handleError]); const refetchData = useCallback(async () => { setIsReFetching(true); @@ -73,8 +78,8 @@ export default function Home() { }, [apiGet]); const handleShuffleClick = () => { - const channelNames = channels.map((channel) => channel.user_name); - const channelName = channelNames[Math.floor(Math.random() * channelNames.length)]; + const channelNames = channels?.map((channel) => channel.user_name); + const channelName = channelNames?.[Math.floor(Math.random() * channelNames.length)]; window.open(`https://www.twitch.tv/${channelName}`, "_blank"); }; @@ -111,18 +116,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)); }); @@ -135,7 +142,7 @@ export default function Home() { ); return ( - + <> @@ -213,7 +220,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/Stats.tsx b/src/pages/Stats.tsx index 45bd9f7..de5c4f3 100644 --- a/src/pages/Stats.tsx +++ b/src/pages/Stats.tsx @@ -19,22 +19,17 @@ import { useAxios } from "../hooks/useAxios"; import { endpoints } from "../service/api"; import { Stats, StatsSummary, StatsSummaryDefault } from "../types"; -import LandingLayout from "../components/layouts/LandingLayout"; - export default function Supporters() { const { apiGet } = useAxios(); const [isLoading, setIsLoading] = useState(false); const [stats, setStats] = useState([]); - const [statsSummary, setStatsSummary] = - useState(StatsSummaryDefault); + const [statsSummary, setStatsSummary] = useState(StatsSummaryDefault); const loadData = useCallback(async () => { setIsLoading(true); const statsList = await apiGet(endpoints.stats.url); - const statsSummaryList = await apiGet( - endpoints.stats_summary.url, - ); + const statsSummaryList = await apiGet(endpoints.stats_summary.url); setStats(statsList); setStatsSummary(statsSummaryList); @@ -64,7 +59,7 @@ export default function Supporters() { }; return ( - + <> Estatísticas Saiba mais sobre o projeto! @@ -105,17 +100,13 @@ export default function Supporters() {
)} -
+ ); } diff --git a/src/pages/Supporters.tsx b/src/pages/Supporters.tsx index 77a7349..850e147 100644 --- a/src/pages/Supporters.tsx +++ b/src/pages/Supporters.tsx @@ -1,20 +1,9 @@ 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"; - export default function Supporters() { const [supporters, setSupporters] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -34,7 +23,7 @@ export default function Supporters() { }, [loadData]); return ( - + <> Agradecimentos Saiba mais sobre o projeto! @@ -76,6 +65,6 @@ export default function Supporters() { ))} )} - + ); } diff --git a/yarn.lock b/yarn.lock index 6ee4b4e..5c6b6b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9412,6 +9412,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" From 29aeaee61a3d5829bf2a9ab1e103ccc170782e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Tue, 22 Mar 2022 21:09:56 -0300 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=F0=9F=A4=96=20error=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/sections/ErrorFallback.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/sections/ErrorFallback.tsx b/src/components/sections/ErrorFallback.tsx index 1a541ef..83adae3 100644 --- a/src/components/sections/ErrorFallback.tsx +++ b/src/components/sections/ErrorFallback.tsx @@ -1,15 +1,19 @@ -import { Center, VStack, Text } from "@chakra-ui/react"; +import { Center, VStack, Text, Button, Box } from "@chakra-ui/react"; +import { FallbackProps } from "react-error-boundary"; -const ErrorFallback = () => { +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { return (
- - - Oops! Algo de errado não está certo - - - Não conseguimos carregar o que você estava procurando 😔 - + + + + Oops! Algo de errado não está certo + + + Não conseguimos carregar o que você estava procurando 😔 + + +
); From fb0ccb9912cb58f74fe1e9f60910cfa83036da3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Thu, 24 Mar 2022 20:32:52 -0300 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20button=20s?= =?UTF-8?q?tyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/sections/ErrorFallback.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/sections/ErrorFallback.tsx b/src/components/sections/ErrorFallback.tsx index 83adae3..6317b3f 100644 --- a/src/components/sections/ErrorFallback.tsx +++ b/src/components/sections/ErrorFallback.tsx @@ -1,4 +1,5 @@ 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) => { @@ -13,7 +14,9 @@ const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { Não conseguimos carregar o que você estava procurando 😔
- + ); From 3064f8de63ff885ffc03a659e8edc7aa41d9f072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Thu, 24 Mar 2022 20:55:23 -0300 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20error=20bound?= =?UTF-8?q?ary=20to=20other=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 25 ++++++++++--------------- src/pages/Home.tsx | 24 ++++++++++++++---------- src/pages/Stats.tsx | 22 +++++++++++++++------- src/pages/Supporters.tsx | 20 ++++++++++++++------ 4 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1ddbbf0..194f602 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,22 +21,17 @@ function App() { return ( - - - - - } - /> - } /> - } /> - } /> - {/* } /> + + + } /> + } /> + } /> + } /> + {/* } /> } /> */} - } /> - + } /> + + ); } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 19ab04d..f4bc432 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -64,18 +64,22 @@ export default function Home() { }, [apiGet, handleError]); const refetchData = useCallback(async () => { - setIsReFetching(true); - - const channelsList = await apiGet(endpoints.channels.url); - const tagsList = await apiGet(endpoints.tags.url); - const vodsList = await apiGet(endpoints.vods.url); + try { + setIsReFetching(true); - setChannels(channelsList); - setTags(tagsList); - setVods(vodsList); + const channelsList = await apiGet(endpoints.channels.url); + const tagsList = await apiGet(endpoints.tags.url); + const vodsList = await apiGet(endpoints.vods.url); - setIsReFetching(false); - }, [apiGet]); + setChannels(channelsList); + setTags(tagsList); + setVods(vodsList); + } catch (error) { + handleError(error); + } finally { + setIsReFetching(false); + } + }, [apiGet, handleError]); const handleShuffleClick = () => { const channelNames = channels?.map((channel) => channel.user_name); diff --git a/src/pages/Stats.tsx b/src/pages/Stats.tsx index de5c4f3..9f28e14 100644 --- a/src/pages/Stats.tsx +++ b/src/pages/Stats.tsx @@ -18,23 +18,31 @@ import { import { useAxios } from "../hooks/useAxios"; import { endpoints } from "../service/api"; import { Stats, StatsSummary, StatsSummaryDefault } from "../types"; +import { useErrorHandler } from "react-error-boundary"; export default function Supporters() { 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(); diff --git a/src/pages/Supporters.tsx b/src/pages/Supporters.tsx index 850e147..cc2b172 100644 --- a/src/pages/Supporters.tsx +++ b/src/pages/Supporters.tsx @@ -3,20 +3,28 @@ import axios from "axios"; import { Box, Center, Heading, Image, Spinner, Text, VStack, Wrap } from "@chakra-ui/react"; import type { Supporter } from "../types"; +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(); From 478ece87b1ea370f6d9f00e8b3410e359498005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Magalh=C3=A3es?= Date: Sun, 10 Apr 2022 23:03:17 -0300 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=F0=9F=92=8D=20error=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/integration/home/error-fallback.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 cypress/integration/home/error-fallback.spec.js 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"); + }); +});