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 😔
+
+
+ }>
+ Tente novamente
+
+
+
+ );
+};
+
+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"