diff --git a/package.json b/package.json index 71a8501..840475a 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,15 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^4.2.1", + "@tanstack/react-query-devtools": "^4.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-infinite-scroller": "^1.2.6", "react-router-dom": "^6.3.0", + "react-toastify": "^9.0.8", "swiper": "^8.1.0", - "swr": "^1.2.2" + "uuid": "^8.3.2" }, "devDependencies": { "@vitejs/plugin-react": "^1.0.7", diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 2a118fc..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Fragment, Suspense, lazy } from "react"; -import { Routes, Route } from "react-router-dom"; -import Main from "./components/layout/Main"; -import LoadingComponent from "./components/loading/LoadingComponent"; - -const HomePage = lazy(() => import("./page/HomePage")); -const AnimePage = lazy(() => import("./page/AnimePage")); -const SearchPage = lazy(() => import("./page/SearchPage")); -const AnimeDetailPage = lazy(() => import("./page/AnimeDetailPage")); -const CharacterPage = lazy(() => import("./page/CharacterPage")); -const CharacterDetailPage = lazy(() => import("./page/CharacterDetailPage")); -const Error404Page = lazy(() => import("./page/Error404Page")); - -import "swiper/css"; - -const App = () => { - return ( - - }> - - }> - } /> - } /> - } /> - } /> - } - /> - } /> - - - } /> - - - - ); -}; - -export default App; diff --git a/src/apis/apis.jsx b/src/apis/apis.jsx new file mode 100644 index 0000000..c76f2f9 --- /dev/null +++ b/src/apis/apis.jsx @@ -0,0 +1,19 @@ +export const getAnimeDetail = async (id) => { + const res = await fetch(`https://api.jikan.moe/v4/anime/${id}`); + return res.json(); +}; + +export const getListAnime = async (type) => { + const res = await fetch(`https://api.jikan.moe/v4/${type}`); + return res.json(); +}; + +export const getCharacterDetail = async (id) => { + const res = await fetch(`https://api.jikan.moe/v4/characters/${id}`); + return res.json(); +}; + +export const search = async (url) => { + const res = await fetch(url); + return res.json(); +}; diff --git a/src/components/anime/AnimeList.jsx b/src/components/anime/AnimeList.jsx index e172fc4..8ea0fed 100644 --- a/src/components/anime/AnimeList.jsx +++ b/src/components/anime/AnimeList.jsx @@ -1,25 +1,32 @@ -import useSWR from "swr"; -import { fetcher } from "../../utils/fetcher"; import { Swiper, SwiperSlide } from "swiper/react"; import AnimeItem from "./AnimeItem"; import AnimeItemSkeleton from "./AnimeItemSkeleton"; +import { getListAnime } from "../../apis/apis"; +import { useQuery } from "@tanstack/react-query"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; -const AnimeList = ({ url }) => { - const { data, error } = useSWR(url, fetcher); - const loading = !data && !error; +const AnimeList = ({ type }) => { + const { data, isError, isLoading } = useQuery(["list-anime", type], () => + getListAnime(type) + ); + const navigate = useNavigate(); - if (error) console.error(error); + if (isError) { + toast.error("Something went wrong! Please try again!"); + return navigate("/"); + } return (
- {loading && + {isLoading && new Array(10).fill(0).map((item, index) => ( ))} - {!loading && + {!isLoading && data && data.data.length > 0 && data.data.map((anime) => ( diff --git a/src/components/layout/Main.jsx b/src/components/layout/ShareLayout.jsx similarity index 77% rename from src/components/layout/Main.jsx rename to src/components/layout/ShareLayout.jsx index 84d696d..28d1489 100644 --- a/src/components/layout/Main.jsx +++ b/src/components/layout/ShareLayout.jsx @@ -2,7 +2,7 @@ import { Outlet } from "react-router-dom"; import Header from "./Header"; import Footer from "./Footer"; -const Main = () => { +const ShareLayout = () => { return ( <>
@@ -12,4 +12,4 @@ const Main = () => { ); }; -export default Main; +export default ShareLayout; diff --git a/src/components/search/SearchContainer.jsx b/src/components/search/SearchContainer.jsx index 8479df4..83b48bc 100644 --- a/src/components/search/SearchContainer.jsx +++ b/src/components/search/SearchContainer.jsx @@ -1,26 +1,42 @@ -import useSWR from "swr"; import AnimeItem from "../anime/AnimeItem"; -import { fetcher } from "../../utils/fetcher"; -import { Swiper, SwiperSlide } from "swiper/react"; import { useEffect, useRef, useState } from "react"; import CharacterItem from "../character/CharacterItem"; import AnimeItemSkeleton from "../anime/AnimeItemSkeleton"; -import CharacterItemSkeleton from "../character/CharacterItemSkeleton"; +import { search } from "../../apis/apis"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; +import { v4 } from "uuid"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import InfiniteScroll from "react-infinite-scroller"; const SearchContainer = ({ type }) => { + const navigate = useNavigate(); const [query, setQuery] = useState("naruto"); const inputRef = useRef(null); const searchBtnRef = useRef(null); - const url = - type === "anime" - ? "https://api.jikan.moe/v4/anime" - : "https://api.jikan.moe/v4/characters"; + const url = `https://api.jikan.moe/v4/${type}?q=${query}`; - const { data, error } = useSWR(`${url}?q=${query}`, fetcher); + const { data, hasNextPage, fetchNextPage, isError, isLoading } = + useInfiniteQuery( + [`search-${type}`, query], + ({ pageParam = url }) => search(pageParam), + { + getNextPageParam: (lastPage, allPages) => + lastPage.pagination.has_next_page + ? `${url}&page=${lastPage.pagination.current_page + 1}` + : undefined, + } + ); + + if (isLoading) { + toast.info("Loading..."); + } - if (error) console.error(error); - const loading = !data && !error; + if (isError) { + toast.error("Something went wrong! Please try again!"); + return navigate("/"); + } useEffect(() => { const handlerEnterKeyPress = (e) => { @@ -36,6 +52,8 @@ const SearchContainer = ({ type }) => { }; }, []); + console.log(data); + return (
@@ -57,37 +75,29 @@ const SearchContainer = ({ type }) => {
- - {loading && - new Array(4) - .fill(0) - .map((item, index) => ( - - {type === "anime" ? ( - - ) : ( - - )} - - ))} - {!loading && - data && - data.data.length > 0 && - data.data.map((item) => ( - - {type === "anime" ? ( - - ) : ( - - )} - + {isLoading && ( +
+ {new Array(4).fill(0).map(() => ( + ))} - {data && data.data.length <= 0 && ( -
- Keyword: {query} is empty! -
- )} - +
+ )} + +
+ {!isLoading && + data.pages.map((pageData) => + pageData.data.map((item) => ( + <> + {type === "anime" ? ( + + ) : ( + + )} + + )) + )} +
+
diff --git a/src/main.jsx b/src/main.jsx index 358d9e6..49b4372 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,58 @@ import { render } from "react-dom"; -import { BrowserRouter } from "react-router-dom"; +import { lazy, Suspense } from "react"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; -import App from "./App"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import LoadingComponent from "./components/loading/LoadingComponent"; +import ShareLayout from "./components/layout/ShareLayout"; + +const HomePage = lazy(() => import("./page/HomePage")); +const AnimePage = lazy(() => import("./page/AnimePage")); +const SearchPage = lazy(() => import("./page/SearchPage")); +const AnimeDetailPage = lazy(() => import("./page/AnimeDetailPage")); +const CharacterPage = lazy(() => import("./page/CharacterPage")); +const CharacterDetailPage = lazy(() => import("./page/CharacterDetailPage")); +const Error404Page = lazy(() => import("./page/Error404Page")); + +import "swiper/css"; +import "react-toastify/dist/ReactToastify.css"; import "./index.scss"; +import { ToastContainer } from "react-toastify"; + +const queryClient = new QueryClient(); render( - - - , + + + }> + + }> + } /> + } /> + } /> + } /> + } + /> + } /> + + + } /> + + + + + , document.getElementById("root") ); diff --git a/src/page/AnimeDetailPage.jsx b/src/page/AnimeDetailPage.jsx index 402faec..d36d5bb 100644 --- a/src/page/AnimeDetailPage.jsx +++ b/src/page/AnimeDetailPage.jsx @@ -1,6 +1,4 @@ -import { useParams } from "react-router-dom"; -import useSWR from "swr"; -import { fetcher } from "../utils/fetcher"; +import { useNavigate, useParams } from "react-router-dom"; import DetailStatus from "../components/anime-details/DetailStatus"; import IconStar from "../components/icons/IconStar"; @@ -11,16 +9,23 @@ import IconRank from "../components/icons/IconRank"; import { getRating } from "../utils/getRating"; import DetailListItem from "../components/anime-details/DetailListItem"; import LoadingComponent from "../components/loading/LoadingComponent"; +import { useQuery } from "@tanstack/react-query"; +import { getAnimeDetail } from "../apis/apis"; +import { toast } from "react-toastify"; const AnimeDetailPage = () => { const { animeID } = useParams(); + const navigate = useNavigate(); - const { data, error } = useSWR( - `https://api.jikan.moe/v4/anime/${animeID}`, - fetcher + const { data, error, isLoading } = useQuery(["anime", animeID], () => + getAnimeDetail(animeID) ); - if (error) console.error(error); - if (!data) return ; + + if (error) { + toast.error("Something went wrong! Please try again!"); + return navigate("/"); + } + if (isLoading) return ; const { images, diff --git a/src/page/AnimePage.jsx b/src/page/AnimePage.jsx index 4cda532..3a1f268 100644 --- a/src/page/AnimePage.jsx +++ b/src/page/AnimePage.jsx @@ -5,17 +5,17 @@ const AnimePage = () => {

Top Anime

- +

Season now

- +

Season Upcoming

- +
); diff --git a/src/page/CharacterDetailPage.jsx b/src/page/CharacterDetailPage.jsx index aff22a8..36c81e5 100644 --- a/src/page/CharacterDetailPage.jsx +++ b/src/page/CharacterDetailPage.jsx @@ -1,19 +1,24 @@ -import useSWR from "swr"; -import { useParams } from "react-router-dom"; -import { fetcher } from "../utils/fetcher"; +import { useNavigate, useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import IconFavorite from "../components/icons/IconFavorite"; import IconEmail from "../components/icons/IconEmail"; import LoadingComponent from "../components/loading/LoadingComponent"; +import { getCharacterDetail } from "../apis/apis"; +import { toast } from "react-toastify"; const CharacterDetailPage = () => { + const navigate = useNavigate(); const { characterID } = useParams(); - const { data, error } = useSWR( - `https://api.jikan.moe/v4/characters/${characterID}`, - fetcher + const { data, isError, isLoading } = useQuery( + ["character", characterID], + () => getCharacterDetail(characterID) ); - if (error) console.error(error); - if (!data) return ; + if (isError) { + toast.error("Something went wrong! Please try again!"); + return navigate("/"); + } + if (isLoading) return ; const { images, name, name_kanji, nicknames, favorites, about, url } = data.data; diff --git a/src/page/Error404Page.jsx b/src/page/Error404Page.jsx index 5260217..5fac6bb 100644 --- a/src/page/Error404Page.jsx +++ b/src/page/Error404Page.jsx @@ -8,11 +8,7 @@ const Error404Page = () => {
- +

Oops!

You need map!

diff --git a/yarn.lock b/yarn.lock index d8cd0b3..2ed25ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -310,6 +310,41 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@tanstack/match-sorter-utils@^8.0.0-alpha.82": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834" + integrity sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A== + dependencies: + remove-accents "0.4.2" + +"@tanstack/query-core@^4.0.0-beta.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.2.1.tgz#21ff3a33f27bf038c990ea53af89cf7c7e8078fc" + integrity sha512-UOyOhHKLS/5i9qG2iUnZNVV3R9riJJmG9eG+hnMFIPT/oRh5UzAfjxCtBneNgPQZLDuP8y6YtRYs/n4qVAD5Ng== + +"@tanstack/react-query-devtools@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.2.1.tgz#decee3d1d174b253fa303d5baaa478fb0e2c0e63" + integrity sha512-k7Ch3qvs8U74aRMMRvNisxcxZFTzk8FDdvpQKXxSZ8fsD4ZwpM0MoUSqKsCXbfTvUI7MJiGxavy1YlvImPNO+Q== + dependencies: + "@tanstack/match-sorter-utils" "^8.0.0-alpha.82" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + +"@tanstack/react-query@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.2.1.tgz#1f00f03573b35a353e62fa64f904bbb0286a1808" + integrity sha512-w02oTOYpoxoBzD/onAGRQNeLAvggLn7WZjS811cT05WAE/4Q3br0PTp388M7tnmyYGbgOOhFq0MkhH0wIfAKqA== + dependencies: + "@tanstack/query-core" "^4.0.0-beta.1" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@vitejs/plugin-react@^1.0.7": version "1.3.2" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz#2fcf0b6ce9bcdcd4cec5c760c199779d5657ece1" @@ -432,6 +467,11 @@ chalk@^2.0.0: optionalDependencies: fsevents "~2.3.2" +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -784,7 +824,7 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== -loose-envify@^1.1.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -917,6 +957,15 @@ postcss@^8.4.12, postcss@^8.4.13, postcss@^8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +prop-types@^15.5.8: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -936,6 +985,18 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-infinite-scroller@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz#8b80233226dc753a597a0eb52621247f49b15f18" + integrity sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ== + dependencies: + prop-types "^15.5.8" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-refresh@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1" @@ -956,6 +1017,13 @@ react-router@6.3.0: dependencies: history "^5.2.0" +react-toastify@^9.0.8: + version "9.0.8" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.0.8.tgz#3876c89fc6211a29027b3075010b5ec39ebe4f7e" + integrity sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ== + dependencies: + clsx "^1.1.1" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -983,6 +1051,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +remove-accents@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" + integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== + resolve@^1.1.7, resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" @@ -1068,11 +1141,6 @@ swiper@^8.1.0: dom7 "^4.0.4" ssr-window "^4.0.2" -swr@^1.2.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" - integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== - tailwindcss@^3.0.23: version "3.1.2" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.2.tgz#b5607d17adb6cbb11a13738cc5fdf3e5527dcd7a" @@ -1113,11 +1181,21 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + vite@^2.9.0: version "2.9.12" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.12.tgz#b1d636b0a8ac636afe9d83e3792d4895509a941b"