diff --git a/src/App.jsx b/src/App.jsx index 484a22f..b1243ce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,10 @@ import { Fragment, Suspense, lazy } from "react"; import { Routes, Route } from "react-router-dom"; import Main from "./components/layout/Main"; -import "react-toastify/dist/ReactToastify.css"; -import "swiper/css"; import LoadingComponent from "./components/loading/LoadingComponent"; +import "swiper/css"; + const HomePage = lazy(() => import("./page/HomePage")); const AnimePage = lazy(() => import("./page/AnimePage")); const SearchPage = lazy(() => import("./page/SearchPage")); diff --git a/src/components/anime-details/DetailStatus.jsx b/src/components/anime-details/DetailStatus.jsx new file mode 100644 index 0000000..2bd7bea --- /dev/null +++ b/src/components/anime-details/DetailStatus.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const DetailStatus = ({ type, content, className }) => { + return ( + + {type}: + {content} + + ); +}; + +export default DetailStatus; diff --git a/src/components/anime/AnimeItem.jsx b/src/components/anime/AnimeItem.jsx index 28ba71b..73c8529 100644 --- a/src/components/anime/AnimeItem.jsx +++ b/src/components/anime/AnimeItem.jsx @@ -1,59 +1,40 @@ import React from "react"; import { useNavigate } from "react-router-dom"; +import IconStar from "../icons/IconStar"; +import IconFavorite from "../icons/IconFavorite"; +import Button from "../buttons/Button"; const AnimeItem = ({ anime }) => { const navigate = useNavigate(); const { title, score, favorites, images, mal_id } = anime; return ( -
-
+
+
-
+

{title}

- {score} - - - + {score || "0"} + +
- {favorites} - - - + {favorites || "0"} +
-
- -
+
); diff --git a/src/components/buttons/Button.jsx b/src/components/buttons/Button.jsx new file mode 100644 index 0000000..2dbd7f0 --- /dev/null +++ b/src/components/buttons/Button.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +const Button = ({ className = "", onClick = () => {}, children }) => { + return ( + + ); +}; + +export default Button; diff --git a/src/components/icons/IconFavorite.jsx b/src/components/icons/IconFavorite.jsx new file mode 100644 index 0000000..ef7da54 --- /dev/null +++ b/src/components/icons/IconFavorite.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const IconFavorite = ({ className }) => { + return ( + + + + ); +}; + +export default IconFavorite; diff --git a/src/components/icons/IconRank.jsx b/src/components/icons/IconRank.jsx new file mode 100644 index 0000000..0560f9e --- /dev/null +++ b/src/components/icons/IconRank.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const IconRank = ({ className }) => { + return ( + + + + ); +}; + +export default IconRank; diff --git a/src/components/icons/IconStar.jsx b/src/components/icons/IconStar.jsx new file mode 100644 index 0000000..bf34a4b --- /dev/null +++ b/src/components/icons/IconStar.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +const IconStar = ({ className }) => { + return ( + + + + ); +}; + +export default IconStar; diff --git a/src/components/icons/IconUserGroup.jsx b/src/components/icons/IconUserGroup.jsx new file mode 100644 index 0000000..798fa8e --- /dev/null +++ b/src/components/icons/IconUserGroup.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +const IconUserGroup = ({ className }) => { + return ( + + + + ); +}; + +export default IconUserGroup; diff --git a/src/components/loading/Loading.jsx b/src/components/loading/Loading.jsx deleted file mode 100644 index 0af1b60..0000000 --- a/src/components/loading/Loading.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Fragment } from "react"; - -const Loading = () => { - return ( - - - - - - - ); -}; - -export default Loading; diff --git a/src/context/mangaContext.jsx b/src/context/mangaContext.jsx deleted file mode 100644 index 6e0f0b3..0000000 --- a/src/context/mangaContext.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createContext, useContext } from "react"; - -const MangaContext = createContext(undefined); - -const MangaProvider = (props) => { - return ; -}; - -const useManga = () => { - const context = useContext(MangaContext); - if (typeof context === "undefined") - throw new Error("useManga must be used within a MangaProvider"); - return context; -}; - -export { MangaProvider, useManga }; diff --git a/src/hooks/useDebounce.jsx b/src/hooks/useDebounce.jsx deleted file mode 100644 index 0c3a3a8..0000000 --- a/src/hooks/useDebounce.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, useState } from "react"; - -export default function useDebounce(value, delay) { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = useState(value); - useEffect( - () => { - // Update debounced value after delay - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - // Cancel the timeout if value changes (also on delay change or unmount) - // This is how we prevent debounced value from updating if value is changed ... - // .. within the delay period. Timeout gets cleared and restarted. - return () => { - clearTimeout(handler); - }; - }, - [value, delay] // Only re-call effect if value or delay changes - ); - return debouncedValue; -} diff --git a/src/index.scss b/src/index.scss index 3ee1313..14e42eb 100644 --- a/src/index.scss +++ b/src/index.scss @@ -7,6 +7,7 @@ @layer base { html { font-family: 'Poppins', sans-serif; + scroll-behavior: smooth; } body { diff --git a/src/main.jsx b/src/main.jsx index 9fa5a36..dc23f0f 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,17 +1,28 @@ import React from "react"; import ReactDOM from "react-dom"; -import "./index.scss"; -import App from "./App"; import { BrowserRouter } from "react-router-dom"; -import { MangaProvider } from "./context/mangaContext"; +import { ToastContainer } from "react-toastify"; + +import App from "./App"; +import "./index.scss"; +import "react-toastify/dist/ReactToastify.css"; ReactDOM.render( - - - - - + + + + , document.getElementById("root") ); diff --git a/src/page/AnimeDetailPage.jsx b/src/page/AnimeDetailPage.jsx index f8e4a68..6c1588a 100644 --- a/src/page/AnimeDetailPage.jsx +++ b/src/page/AnimeDetailPage.jsx @@ -1,7 +1,13 @@ -import React from "react"; import { useParams } from "react-router-dom"; import useSWR from "swr"; import { fetcher } from "../utils/fetcher"; +import { toast } from "react-toastify"; +import DetailStatus from "../components/anime-details/DetailStatus"; +import IconStar from "../components/icons/IconStar"; +import IconUserGroup from "../components/icons/IconUserGroup"; +import IconFavorite from "../components/icons/IconFavorite"; +import IconRank from "../components/icons/IconRank"; +import { getRating } from "../utils/getRating"; const AnimeDetailPage = () => { const { animeID } = useParams(); @@ -11,29 +17,141 @@ const AnimeDetailPage = () => { fetcher ); + if (error) { + toast.error("Error! Please try again!"); + return null; + } + if (!data) return null; - const { images, title, title_japanese, synopsis } = data.data; + const { + images, + title, + title_japanese, + synopsis, + url, + trailer, + type, + source, + episodes, + status, + duration, + rating, + year, + score, + favorites, + members, + rank, + } = data.data; return ( -
-
- -
-
-
-

{title}

-

{title_japanese}

+
+
+ +
+
+
+

+ {title} ({year || "Empty year"}) +

+

+ {title_japanese} - {`Rank: ${rank}`} +

+
+ +
+
+
+ + {rank || "null"} +
+ +
+ {score || "0"} + +
+ +
+ {favorites || "0"} + +
-
-

{synopsis}

+
+ {members || "0"} + +
+
+ +
+
+ +
+ + + + +
+ +
+ + + +
+
+ +
+

+ Description: + + {synopsis || "Description's empty"} + +

+
+ + {trailer.embed_url && ( +
+ +
+ )}
); }; diff --git a/src/page/HomePage.jsx b/src/page/HomePage.jsx index f839017..15e72b7 100644 --- a/src/page/HomePage.jsx +++ b/src/page/HomePage.jsx @@ -1,17 +1,19 @@ import React from "react"; -import AnimeList from "../components/anime/AnimeList"; const HomePage = () => { return ( -
-
-

Top Anime

- -
- -
-

Top Manga

- +
+
+

+ Project: Anime page with reactJS + Tailwindcss by SoSmoothy +

+
+ +
); diff --git a/src/page/SearchPage.jsx b/src/page/SearchPage.jsx index 0e09c7b..86941e4 100644 --- a/src/page/SearchPage.jsx +++ b/src/page/SearchPage.jsx @@ -1,11 +1,12 @@ -import React from "react"; +import { useState, useRef } from "react"; import useSWR from "swr"; import { fetcher } from "../utils/fetcher"; import AnimeItem from "../components/anime/AnimeItem"; +import { toast } from "react-toastify"; const SearchPage = () => { - const [query, setQuery] = React.useState("naruto"); - const inputRef = React.useRef(null); + const [query, setQuery] = useState("naruto"); + const inputRef = useRef(null); const handleSearchBtn = (e) => { const searchVal = inputRef.current.value; @@ -17,6 +18,8 @@ const SearchPage = () => { fetcher ); + if (error) toast.error("Error! Please try again!"); + return (
@@ -30,13 +33,13 @@ const SearchPage = () => { />
-
+
{data && data.data.length > 0 && data.data.map((anime) => ( diff --git a/src/utils/getRating.jsx b/src/utils/getRating.jsx new file mode 100644 index 0000000..9eba760 --- /dev/null +++ b/src/utils/getRating.jsx @@ -0,0 +1,4 @@ +export const getRating = (rating = "") => { + if (!rating) return ""; + return rating.split(" ")[0]; +};