TV 쇼 React 앱을 만드는 방법 - tvmaze

오늘은 검색창을 이용하여 TV쇼의 정보를 보여주는 어플리케이션을 만들어 보도록 하겠습니다.

이 프로젝트는
  • 반응 스크립트(스타일)
  • 반응 스피너(로더)
  • axios(Http 요청)
  • 반응 아이콘(아이콘)
  • 프레이머 모션(애니메이션)

  • API: https://www.tvmaze.com/api

    Github의 코드: https://github.com/rodrigolazo/react-tvmaze

    프로젝트 구조


    암호:
    App.js

    import styled from "styled-components";
    import "./App.css";
    import Header from "./components/header";
    import { SearchBar } from "./components/searchBar";
    
    const AppContainer = styled.div`
      margin: auto;
      padding: 0 530px;
    `;
    
    function App() {
      return (
        <>
          <AppContainer>
            <Header />
            <SearchBar />
          </AppContainer>
        </>
      );
    }
    
    export default App;
    
    


    헤더/index.jsx

    import React from 'react'
    import logo from '../../img/logo.png'
    
    const Header = () => {
      return (
        <header className='center'>
          <img src={logo} alt='' />
        </header>
      )
    }
    
    export default Header
    


    searchBar/index.jsx

    import React from "react";
    import styled from "styled-components";
    import { IoClose, IoSearch } from "react-icons/io5";
    import { useState } from "react";
    import { AnimatePresence, motion } from "framer-motion";
    import { useClickOutside } from "react-click-outside-hook";
    import { useEffect } from "react";
    import { useRef } from "react";
    import BeatLoader from "react-spinners/BeatLoader";
    import { useDebounce } from "../../hooks/debounceHook";
    import axios from "axios";
    import { TvShow } from "../tvShow";
    
    const SearchBarContainer = styled(motion.div)`
      display: flex;
      flex-direction: column;
      width: 34em;
      height: 3.8em;
      background-color: #fff;
      border-radius: 6px;
      box-shadow: 0px 2px 12px 3px rgba(0, 0, 0, 0.14);
    `;
    
    const SearchInputContainer = styled.div`
      width: 100%;
      min-height: 4em;
      display: flex;
      align-items: center;
      position: relative;
      padding: 2px 15px;
    `;
    
    const SearchInput = styled.input`
      width: 100%;
      height: 100%;
      outline: none;
      border: none;
      font-size: 21px;
      color: #12112e;
      font-weight: 500;
      border-radius: 6px;
      background-color: transparent;
    
      &:focus {
        outline: none;
        &::placeholder {
          opacity: 0;
        }
      }
    
      &::placeholder {
        color: #bebebe;
        transition: all 250ms ease-in-out;
      }
    `;
    
    const SearchIcon = styled.span`
      color: #bebebe;
      font-size: 27px;
      margin-right: 10px;
      margin-top: 6px;
      vertical-align: middle;
    `;
    
    const CloseIcon = styled(motion.span)`
      color: #bebebe;
      font-size: 23px;
      vertical-align: middle;
      transition: all 200ms ease-in-out;
      cursor: pointer;
    
      &:hover {
        color: #dfdfdf;
      }
    `;
    
    const LineSeperator = styled.span`
      display: flex;
      min-width: 100%;
      min-height: 2px;
      background-color: #d8d8d878;
    `;
    
    const SearchContent = styled.div`
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: column;
      padding: 1em;
      overflow-y: auto;
    `;
    
    const LoadingWrapper = styled.div`
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    `;
    
    const WarningMessage = styled.span`
      color: #a1a1a1;
      font-size: 14px;
      display: flex;
      align-self: center;
      justify-self: center;
    `;
    
    const containerVariants = {
      expanded: {
        height: "30em",
      },
      collapsed: {
        height: "3.8em",
      },
    };
    
    const containerTransition = { type: "spring", damping: 22, stiffness: 150 };
    
    export function SearchBar(props) {
      const [isExpanded, setExpanded] = useState(false);
      const [parentRef, isClickedOutside] = useClickOutside();
      const inputRef = useRef();
      const [searchQuery, setSearchQuery] = useState("");
      const [isLoading, setLoading] = useState(false);
      const [tvShows, setTvShows] = useState([]);
      const [noTvShows, setNoTvShows] = useState(false);
    
      const isEmpty = !tvShows || tvShows.length === 0;
    
      const changeHandler = (e) => {
        e.preventDefault();
        if (e.target.value.trim() === "") setNoTvShows(false);
    
        setSearchQuery(e.target.value);
      };
    
      const expandContainer = () => {
        setExpanded(true);
      };
    
      const collapseContainer = () => {
        setExpanded(false);
        setSearchQuery("");
        setLoading(false);
        setNoTvShows(false);
        setTvShows([]);
        if (inputRef.current) inputRef.current.value = "";
      };
    
      useEffect(() => {
        if (isClickedOutside) collapseContainer();
      }, [isClickedOutside]);
    
      const prepareSearchQuery = (query) => {
        const url = `http://api.tvmaze.com/search/shows?q=${query}`;
    
        return encodeURI(url);
      };
    
      const searchTvShow = async () => {
        if (!searchQuery || searchQuery.trim() === "") return;
    
        setLoading(true);
        setNoTvShows(false);
    
        const URL = prepareSearchQuery(searchQuery);
    
        const response = await axios.get(URL).catch((err) => {
          console.log("Error: ", err);
        });
    
        if (response) {
          console.log("Response: ", response.data);
          if (response.data && response.data.length === 0) setNoTvShows(true);
    
          setTvShows(response.data);
        }
    
        setLoading(false);
      };
    
      useDebounce(searchQuery, 500, searchTvShow);
    
      return (
        <SearchBarContainer
          animate={isExpanded ? "expanded" : "collapsed"}
          variants={containerVariants}
          transition={containerTransition}
          ref={parentRef}
        >
          <SearchInputContainer>
            <SearchIcon>
              <IoSearch />
            </SearchIcon>
            <SearchInput
              placeholder="Search for series and TvShow"
              onFocus={expandContainer}
              ref={inputRef}
              value={searchQuery}
              onChange={changeHandler}
            />
            <AnimatePresence>
              {isExpanded && (
                <CloseIcon
                  key="close-icon"
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  onClick={collapseContainer}
                  transition={{ duration: 0.2 }}
                >
                  <IoClose />
                </CloseIcon>
              )}
            </AnimatePresence>
          </SearchInputContainer>
          {isExpanded && <LineSeperator />}
          {isExpanded && (
            <SearchContent>
              {isLoading && (
                <LoadingWrapper>
                  <BeatLoader loading color="#3C948B" size={10} />
                </LoadingWrapper>
              )}
              {!isLoading && isEmpty && !noTvShows && (
                <LoadingWrapper>
                  <WarningMessage>Start typing to Search</WarningMessage>
                </LoadingWrapper>
              )}
              {!isLoading && noTvShows && (
                <LoadingWrapper>
                  <WarningMessage>No series or Tv Shows  found!</WarningMessage>
                </LoadingWrapper>
              )}
              {!isLoading && !isEmpty && (
                <>
                  {tvShows.map(({ show }) => (
                    <TvShow
                      key={show.id}
                      thumbanilSrc={show.image && show.image.medium}
                      name={show.name}
                      rating={show.rating && show.rating.average}
                      url={show.url}
                    />
                  ))}
                </>
              )}
            </SearchContent>
          )}
        </SearchBarContainer>
      );
    }
    
    


    tvShow/index.jsx

    import React from "react";
    import styled from "styled-components";
    
    const TvShowContainer = styled.div`
      width: 100%;
      min-height: 6em;
      display: flex;
      border-bottom: 2px solid #d8d8d852;
      padding: 6px 8px;
      align-items: center;
    
      &:hover {
        background-color: #dadada;
        transition: all 0.3s ease;
        border-radius: 3px;
      }
    `;
    
    const Thumbnail = styled.div`
      width: auto;
      height: 100%;
      display: flex;
      flex: 0.4;
    
      img {
        width: auto;
        height: 100%;
      }
    `;
    
    const Name = styled.h3`
      font-size: 15px;
      color: #000;
      margin-left: 10px;
      flex: 2;
      display: flex;
    `;
    
    const Rating = styled.span`
      color: #a1a1a1;
      font-size: 16px;
      display: flex;
      flex: 0.2;
    `;
    
    export function TvShow(props) {
      const { thumbanilSrc, name, rating, url } = props;
    
      return (
        <TvShowContainer>
          <Thumbnail>
            <img src={thumbanilSrc} />
          </Thumbnail>
    
          <Name>
            <a href={url} target="_blank">
              {name}
            </a>
          </Name>
          <Rating>{rating || "N/A"}</Rating>
        </TvShowContainer>
      );
    }
    
    


    후크/debounceHook.jsx

    import React from "react";
    import { useEffect } from "react";
    import { useState } from "react";
    
    export function useDebounce(value, timeout, callback) {
      const [timer, setTimer] = useState(null);
    
      const clearTimer = () => {
        if (timer) clearTimeout(timer);
      };
    
      useEffect(() => {
        clearTimer();
    
        if (value && callback) {
          const newTimer = setTimeout(callback, timeout);
          setTimer(newTimer);
        }
      }, [value]);
    }
    
    


    스타일
    App.css

    body {
      background: #000 url('img/bg.jpg') no-repeat center center/cover;
      font-family: Arial, Helvetica, sans-serif;
    }
    
    .App {
      text-align: center;
    }
    
    .App-logo {
      height: 40vmin;
      pointer-events: none;
    }
    
    @media (prefers-reduced-motion: no-preference) {
      .App-logo {
        animation: App-logo-spin infinite 20s linear;
      }
    }
    
    .App-header {
      background-color: #282c34;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: calc(10px + 2vmin);
      color: white;
    }
    
    .App-link {
      color: #61dafb;
    }
    
    @keyframes App-logo-spin {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }
    
    
    header {
      height: 200px;
    }
    
    header img {
      width: 300px;
    }
    
    .center {
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    a {
      color: #474747;
      text-decoration: none;
    }
    


    결과




    적용된 스타일을 테스트하려면 프로젝트를 다운로드하세요. 도움이 되었으면 합니다.

    Github: https://github.com/rodrigolazo/react-tvmaze

    참조:

    좋은 웹페이지 즐겨찾기