diff --git a/src/components/anime-details/DetailListItem.jsx b/src/components/anime-details/DetailListItem.jsx index 34315d1..5380101 100644 --- a/src/components/anime-details/DetailListItem.jsx +++ b/src/components/anime-details/DetailListItem.jsx @@ -4,14 +4,12 @@ import { NavLink } from 'react-router-dom'; const DetailListItem = ({ items, title }) => { return (
-

{title}:

+

{title}:

{items.map((item) => ( - - - {item.name} - - + + {item.name} + ))}
diff --git a/src/components/anime/AnimeItem.jsx b/src/components/anime/AnimeItem.jsx index 96aac13..fbf6bd9 100644 --- a/src/components/anime/AnimeItem.jsx +++ b/src/components/anime/AnimeItem.jsx @@ -1,30 +1,32 @@ -import { useNavigate } from 'react-router-dom'; import { memo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Button from '../buttons/Button'; 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}

+
+

{title}

{score || '0'} - +
diff --git a/src/components/anime/AnimeItemSkeleton.jsx b/src/components/anime/AnimeItemSkeleton.jsx index ea6826e..bb7c9d9 100644 --- a/src/components/anime/AnimeItemSkeleton.jsx +++ b/src/components/anime/AnimeItemSkeleton.jsx @@ -1,21 +1,24 @@ +import { memo } from 'react'; + import IconStar from '../icons/IconStar'; import IconFavorite from '../icons/IconFavorite'; import LoadingSkeleton from '../loading/LoadingSkeleton'; -import { memo } from 'react'; const AnimeItemSkeleton = () => { return ( -
+
-
+
- +
diff --git a/src/components/anime/AnimeList.jsx b/src/components/anime/AnimeList.jsx index 1b40c6a..14abb2e 100644 --- a/src/components/anime/AnimeList.jsx +++ b/src/components/anime/AnimeList.jsx @@ -1,15 +1,19 @@ +import { memo } from 'react'; +import { toast } from 'react-toastify'; +import { useNavigate } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import { Swiper, SwiperSlide } from 'swiper/react'; + +import { getListAnime } from '../../apis/apis'; + 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'; -import { memo } from 'react'; const AnimeList = ({ type }) => { - const { data, isError, isLoading } = useQuery(['list-anime', type], () => getListAnime(type)); const navigate = useNavigate(); + const { data, isError, isLoading } = useQuery(['list-anime', { type }], () => + getListAnime(type), + ); if (isError) { toast.error('Something went wrong! Please try again!'); @@ -20,7 +24,7 @@ const AnimeList = ({ type }) => {
{isLoading && - new Array(10).fill(0).map((item, index) => ( + new Array(10).fill(0).map((_, index) => ( diff --git a/src/components/character/CharacterItem.jsx b/src/components/character/CharacterItem.jsx index e8d8efe..ab2c52f 100644 --- a/src/components/character/CharacterItem.jsx +++ b/src/components/character/CharacterItem.jsx @@ -1,30 +1,32 @@ import { memo } from 'react'; -import Button from '../buttons/Button'; import { useNavigate } from 'react-router-dom'; + +import Button from '../buttons/Button'; import IconFavorite from '../icons/IconFavorite'; -const CharacterItem = ({ character }) => { +const CharacterItem = ({ character: { mal_id, images, name, favorites } }) => { const navigate = useNavigate(); - const { mal_id, images, name, favorites } = character; return ( -
+
-
-

{name}

-
+
+

{name}

+
{favorites || '0'}
- +
); diff --git a/src/components/character/CharacterItemSkeleton.jsx b/src/components/character/CharacterItemSkeleton.jsx index ac260a3..0a71891 100644 --- a/src/components/character/CharacterItemSkeleton.jsx +++ b/src/components/character/CharacterItemSkeleton.jsx @@ -1,16 +1,17 @@ import { memo } from 'react'; + import IconFavorite from '../icons/IconFavorite'; import LoadingSkeleton from '../loading/LoadingSkeleton'; const CharacterItemSkeleton = () => { return ( -
+
-
+
-
+
diff --git a/src/components/header/HeaderHamburger.jsx b/src/components/header/HeaderHamburger.jsx index c6571dd..ead06c8 100644 --- a/src/components/header/HeaderHamburger.jsx +++ b/src/components/header/HeaderHamburger.jsx @@ -8,7 +8,11 @@ const HeaderHamburger = ({ show }) => { show && 'rotate-45 translate-y-[14px]' } transition-all`} /> - + { return ( -
  • +
  • `p-3 ${isActive && 'bg-green-500'} text-white rounded-lg`} + className={({ isActive }) => + `p-3 ${isActive && 'bg-green-500'} text-white rounded-lg` + } to={to} onClick={() => setShow(false)} > diff --git a/src/components/index.jsx b/src/components/index.jsx index 7fc7931..36e9cc0 100644 --- a/src/components/index.jsx +++ b/src/components/index.jsx @@ -1,23 +1,23 @@ import AnimeItem from './anime/AnimeItem'; -import AnimeItemSkeleton from './anime/AnimeItemSkeleton'; import AnimeList from './anime/AnimeList'; +import AnimeItemSkeleton from './anime/AnimeItemSkeleton'; -import DetailListItem from './anime-details/DetailListItem'; import DetailStatus from './anime-details/DetailStatus'; +import DetailListItem from './anime-details/DetailListItem'; import Button from './buttons/Button'; import CharacterItem from './character/CharacterItem'; import CharacterItemSkeleton from './character/CharacterItemSkeleton'; -import HeaderHamburger from './header/HeaderHamburger'; import HeaderLink from './header/HeaderLink'; +import HeaderHamburger from './header/HeaderHamburger'; +import IconStar from './icons/IconStar'; +import IconRank from './icons/IconRank'; import IconEmail from './icons/IconEmail'; -import IconFavorite from './icons/IconFavorite'; import IconPhone from './icons/IconPhone'; -import IconRank from './icons/IconRank'; -import IconStar from './icons/IconStar'; +import IconFavorite from './icons/IconFavorite'; import IconUserGroup from './icons/IconUserGroup'; import ShareLayout from './layout/ShareLayout'; diff --git a/src/components/layout/Footer.jsx b/src/components/layout/Footer.jsx index dc8f573..e8cb5bb 100644 --- a/src/components/layout/Footer.jsx +++ b/src/components/layout/Footer.jsx @@ -1,27 +1,28 @@ import { memo } from 'react'; + import IconEmail from '../icons/IconEmail'; import IconPhone from '../icons/IconPhone'; const Footer = () => { return (
    -
    -
    +
    +

    - “There are no regrets. If one can be proud of one’s life, one should not wish for - another chance.” + “There are no regrets. If one can be proud of one’s life, one should + not wish for another chance.”

    -

    Saber (Fate Stay Night)

    +

    Saber (Fate Stay Night)

    -
    +

    Contact

    -
    +
    phamanhduc2k2@gmail.com
    -
    +
    +843092002
    diff --git a/src/components/layout/Header.jsx b/src/components/layout/Header.jsx index 5836ded..d047ef3 100644 --- a/src/components/layout/Header.jsx +++ b/src/components/layout/Header.jsx @@ -1,5 +1,6 @@ import { memo, useState } from 'react'; import { NavLink } from 'react-router-dom'; + import HeaderLink from '../header/HeaderLink'; import HeaderHamburger from '../header/HeaderHamburger'; @@ -25,13 +26,15 @@ const Header = () => { const [show, setShow] = useState(false); return ( -
    -
    +
    +
    `p-3 ${isActive && 'bg-green-500'} text-white rounded-lg`} + className={({ isActive }) => + `p-3 ${isActive && 'bg-green-500'} text-white rounded-lg` + } to={'/'} > -

    Home

    +

    Home

    { return ( diff --git a/src/components/loading/LoadingComponent.jsx b/src/components/loading/LoadingComponent.jsx index 2b34a29..cf546ca 100644 --- a/src/components/loading/LoadingComponent.jsx +++ b/src/components/loading/LoadingComponent.jsx @@ -2,22 +2,22 @@ import { memo } from 'react'; const LoadingComponent = () => { return ( -
    +
    diff --git a/src/hooks/useBindEnter.jsx b/src/hooks/useBindEnter.jsx new file mode 100644 index 0000000..d65e622 --- /dev/null +++ b/src/hooks/useBindEnter.jsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; + +const useBindEnter = (setQuery = () => {}, inputRef) => { + useEffect(() => { + const handlerEnterKeyPress = (e) => { + if (e.code === 'Enter') { + setQuery(inputRef.current.value); + } + }; + + document.addEventListener('keyup', handlerEnterKeyPress); + + return () => { + document.removeEventListener('keyup', handlerEnterKeyPress); + }; + }, []); +}; + +export default useBindEnter; diff --git a/src/hooks/useGetAnimeDetail.jsx b/src/hooks/useGetAnimeDetail.jsx new file mode 100644 index 0000000..0ffc7e6 --- /dev/null +++ b/src/hooks/useGetAnimeDetail.jsx @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; + +const useGetAnimeDetail = (animeID) => { + return useQuery(['anime', { animeID }], async () => + (await fetch(`https://api.jikan.moe/v4/anime/${animeID}`)).json(), + ); +}; + +export default useGetAnimeDetail; diff --git a/src/hooks/useGetCharacterDetail.jsx b/src/hooks/useGetCharacterDetail.jsx new file mode 100644 index 0000000..d0dc10b --- /dev/null +++ b/src/hooks/useGetCharacterDetail.jsx @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; + +const useGetCharacterDetail = (characterID) => { + return useQuery(['character', { characterID }], async () => + (await fetch(`https://api.jikan.moe/v4/characters/${characterID}`)).json(), + ); +}; + +export default useGetCharacterDetail; diff --git a/src/hooks/useSearchAnime.jsx b/src/hooks/useSearchAnime.jsx new file mode 100644 index 0000000..a5fd3b2 --- /dev/null +++ b/src/hooks/useSearchAnime.jsx @@ -0,0 +1,18 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { search } from '../apis/apis'; + +const useSearchAnime = (type, query, url) => { + return useInfiniteQuery( + ['search', { type, query }], + ({ pageParam = url }) => search(pageParam), + { + getNextPageParam: (lastPage, _) => + lastPage.pagination.has_next_page + ? `${url}&page=${lastPage.pagination.current_page + 1}` + : undefined, + }, + ); +}; + +export default useSearchAnime; diff --git a/src/main.jsx b/src/main.jsx index 9a4eb31..d136134 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -4,15 +4,15 @@ import { ToastContainer } from 'react-toastify'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import LoadingComponent from './components/loading/LoadingComponent'; import ShareLayout from './components/layout/ShareLayout'; +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 Error404Page = lazy(() => import('./page/Error404Page')); const AnimeDetailPage = lazy(() => import('./page/AnimeDetailPage')); const CharacterDetailPage = lazy(() => import('./page/CharacterDetailPage')); -const Error404Page = lazy(() => import('./page/Error404Page')); import 'swiper/css'; import 'react-toastify/dist/ReactToastify.css'; @@ -29,8 +29,14 @@ render( } /> } /> } /> - } /> - } /> + } + /> + } + /> } /> @@ -49,5 +55,5 @@ render( , - document.getElementById('root') + document.getElementById('root'), ); diff --git a/src/page/AnimeDetailPage.jsx b/src/page/AnimeDetailPage.jsx index ee6bbc0..bab66e3 100644 --- a/src/page/AnimeDetailPage.jsx +++ b/src/page/AnimeDetailPage.jsx @@ -1,9 +1,7 @@ +import { toast } from 'react-toastify'; import { useNavigate, useParams } from 'react-router-dom'; import { getRating } from '../utils/getRating'; -import { useQuery } from '@tanstack/react-query'; -import { getAnimeDetail } from '../apis/apis'; -import { toast } from 'react-toastify'; import { DetailListItem, @@ -15,16 +13,19 @@ import { LoadingComponent, } from '../components'; +import useGetAnimeDetail from '../hooks/useGetAnimeDetail'; + const AnimeDetailPage = () => { const { animeID } = useParams(); const navigate = useNavigate(); - const { data, error, isLoading } = useQuery(['anime', animeID], () => getAnimeDetail(animeID)); + const { data, error, isLoading } = useGetAnimeDetail(animeID); if (error) { toast.error('Something went wrong! Please try again!'); return navigate('/'); } + if (isLoading) return ; const { @@ -52,23 +53,23 @@ const AnimeDetailPage = () => {
    {data && (
    -
    +
    -

    +

    {title} ({year || 'Empty year'})

    -

    +

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

    @@ -97,11 +98,21 @@ const AnimeDetailPage = () => {
    - {genres.length > 0 && } + {genres.length > 0 && ( + + )}
    - - + + {
    - - + + {
    -
    +

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

    diff --git a/src/page/AnimePage.jsx b/src/page/AnimePage.jsx index aa4e169..d7d28a7 100644 --- a/src/page/AnimePage.jsx +++ b/src/page/AnimePage.jsx @@ -2,19 +2,19 @@ import { AnimeList } from '../components'; const AnimePage = () => { return ( -
    +
    -

    Top Anime

    +

    Top Anime

    -

    Season now

    +

    Season now

    -

    Season Upcoming

    +

    Season Upcoming

    diff --git a/src/page/CharacterDetailPage.jsx b/src/page/CharacterDetailPage.jsx index 59b75c4..e1cb4c6 100644 --- a/src/page/CharacterDetailPage.jsx +++ b/src/page/CharacterDetailPage.jsx @@ -1,42 +1,42 @@ -import { useNavigate, useParams } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; import { toast } from 'react-toastify'; - -import { getCharacterDetail } from '../apis/apis'; +import { useNavigate, useParams } from 'react-router-dom'; import { IconEmail, IconFavorite, LoadingComponent } from '../components'; +import useGetCharacterDetail from '../hooks/useGetCharacterDetail'; + const CharacterDetailPage = () => { const navigate = useNavigate(); const { characterID } = useParams(); - const { data, isError, isLoading } = useQuery(['character', characterID], () => - getCharacterDetail(characterID) - ); + const { data, isError, isLoading } = useGetCharacterDetail(characterID); + 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; + const { images, name, name_kanji, nicknames, favorites, about, url } = + data.data; return (
    {data && (
    -
    +
    -

    +

    {name} ({name_kanji || 'empty japanese name'})

    @@ -53,10 +53,12 @@ const CharacterDetailPage = () => {
    -
    +

    About: - {about || "Description's empty"} + + {about || "Description's empty"} +

    diff --git a/src/page/Error404Page.jsx b/src/page/Error404Page.jsx index 118817b..436b8b8 100644 --- a/src/page/Error404Page.jsx +++ b/src/page/Error404Page.jsx @@ -1,19 +1,23 @@ import { useNavigate } from 'react-router-dom'; -import errorImg from '../images/404.png'; import { Button } from '../components'; +import errorImg from '../images/404.png'; + const Error404Page = () => { const navigate = useNavigate(); return ( -
    +
    - +

    Oops!

    You need map!

    -
    diff --git a/src/page/HomePage.jsx b/src/page/HomePage.jsx index 522334c..3a2d8f3 100644 --- a/src/page/HomePage.jsx +++ b/src/page/HomePage.jsx @@ -1,6 +1,6 @@ const HomePage = () => { return ( -
    +

    Project: Anime page with reactJS + Tailwindcss by SoSmoothy @@ -9,7 +9,7 @@ const HomePage = () => {

    diff --git a/src/page/SearchPage.jsx b/src/page/SearchPage.jsx index 1160c81..987a9f1 100644 --- a/src/page/SearchPage.jsx +++ b/src/page/SearchPage.jsx @@ -1,28 +1,30 @@ import { v4 } from 'uuid'; import { toast } from 'react-toastify'; import { useNavigate } from 'react-router-dom'; -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; import InfiniteScroll from 'react-infinite-scroller'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { search } from '../apis/apis'; import { AnimeItem, AnimeItemSkeleton, CharacterItem } from '../components'; +import useSearchAnime from '../hooks/useSearchAnime'; + const SearchPage = ({ type }) => { const navigate = useNavigate(); const [query, setQuery] = useState('naruto'); + const inputRef = useRef(null); const searchBtnRef = useRef(null); const url = `https://api.jikan.moe/v4/${type}?q=${query}`; - const { data, hasNextPage, fetchNextPage, isError, isLoading, isFetchingNextPage } = - useInfiniteQuery([`search-${type}`, query], ({ pageParam = url }) => search(pageParam), { - getNextPageParam: (lastPage, _) => - lastPage.pagination.has_next_page - ? `${url}&page=${lastPage.pagination.current_page + 1}` - : undefined, - }); + const { + data, + hasNextPage, + fetchNextPage, + isError, + isLoading, + isFetchingNextPage, + } = useSearchAnime(type, query, url); if (isError) { toast.error('Something went wrong! Please try again!'); @@ -44,12 +46,12 @@ const SearchPage = ({ type }) => { }, []); return ( -
    +
    -
    +
    {
    -
    +
    {isLoading && ( -
    +
    {new Array(4).fill(0).map(() => ( ))}
    )} -
    +
    {!isLoading && data.pages.map((pageData) => pageData.data.map((item) => ( <> {type === 'anime' ? ( - + ) : ( - + )} - )) + )), )} {isFetchingNextPage && }