DEV Community

Cover image for How to create a TV Show React App - tvmaze
Rodrigo Lazo
Rodrigo Lazo

Posted on

How to create a TV Show React App - tvmaze

Today, We are going to create an application that shows the information of TV Show, using a search bar.

This project uses

  • react-scripts (Styles)
  • react-spinners (Loader)
  • axios (request Http)
  • react-icons (Icons)
  • framer-motion (Animations)

Api: https://www.tvmaze.com/api

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

project structure
Image description

Code:
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;

Enter fullscreen mode Exit fullscreen mode

Header/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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

hooks/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]);
}

Enter fullscreen mode Exit fullscreen mode

Style
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;
}
Enter fullscreen mode Exit fullscreen mode

Results
Image description
Image description
Image description

Download the project to test the applied styles, I hope it helps you

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

references:
https://www.youtube.com/watch?v=x7niho285qs
https://www.youtube.com/watch?v=IlnmWntmUns&t=2974s

Top comments (3)

Collapse
 
techupnewcom profile image
KeithBass

Thank you for sharing, currently I am also watching TV shows on the TV MIX app which is very interesting. You can download this application here: modilimitado.com/tv-mix-apk/

Collapse
 
benchaoui1 profile image
GERMAN IPTV

Mit GERMAN-IPTV öffnen Sie eine neue Welt des Entertainment und der Qualität. Als führende Website in Deutschland sind wir stolz darauf, unseren Kunden die besten Dienstleistungen und die neuesten Inhalte anzubieten. Erhalten Sie unbegrenzten Zugang zu all Ihren Lieblings deutschen Kanälen, sowie zu den neuesten Filmen und Serien, mit einer kostenlosen 3-monatigen Testphase für unsere neuen Kunden. Abonnieren Sie jetzt und genießen Sie die beste GERMAN-IPTV Erfahrung in Deutschland!

Collapse
 
klauswoolhouse profile image
klauswoolhouse

I am absolutely sure that this system is great for watching TV shows. Anyway, this is exactlu what I was looking for! TYSM!