YeonFlix day 03

thisisyjin·2022년 5월 13일
0

Dev Log 🐥

목록 보기
7/23

Day 03 - 220513

  1. 각 컴포넌트 클래스(className) 할당
  2. 스타일 적용 (module.css)
  3. 배포 (gh-pages)

배포 링크

📝 기획안 보기
🎨 디자인 보기


클래스 할당 + module.css

Component

1-1) Load.js

import styles from './Load.module.css';

const Load = () => {
  return (
    <div className={styles.Load}>
      <h1>
        Loading <div className={styles.typing}>. . .</div>
      </h1>
    </div>
  );
};

export default Load;

1-2) Load.module.css

.Load {
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
}

.typing {
  margin: 0;
  white-space: nowrap;
  animation-name: typingDot;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-timing-function: steps(31);
  overflow: hidden;
}

@keyframes typingDot {
  0% {
    width: 0%;
  }
  100% {
    width: 100%;
  }
}
  • Loading ... 에서 '...'을 타이핑 효과를 주듯이 함.
    -> 애니메이션 적용. (width를 0에서 100%로 조절)
    margin: 0을 주고, white-space를 nowrap으로 해줌.

white-space는 스페이스와 탭, 줄바꿈, 자동줄바꿈을 어떻게 처리할지 정하는 속성이다.
-> 기본값 : normal
-> nowrap : 줄바꿈 or 연속스페이스를 하나의 스페이스로. 줄바꿈은 X.


2-1) Navbar.js

import { Link } from 'react-router-dom';
import { Group_key_arr, Group_obj } from '../atom/NavList';
import styles from './Navbar.module.css';

const Navbar = () => {
  return (
    <div className={styles.container}>
      {/*  Page Name */}
      <div className={styles.pageName}>
        <Link to="/">YeonFlix</Link>
      </div>

      {/* Group Links */}
      <div className={styles.groupLink}>
        {Group_key_arr.map((groupName) => {
          return (
            <div key={groupName} className={styles.Link}>
              <div className={styles.LinkSep}>
                <Link to={`/page/${Group_obj[groupName]}/1`}>{groupName}</Link>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default Navbar;

2-2) Navbar.module.css

/* container pageName groupLink Link LinkSep */

.container {
  position: relative;
  width: 100%;
  height: 80px;
  padding: 0 30px;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  z-index: 10;
  transition: background-color 0.25s ease-in-out;
}

.container:hover {
  background-color: #4000a7a9;
}

.pageName {
  position: relative;
  font-size: 36px;
  color: #fff;
  margin-left: 30px;
  transition: transform 0.1s ease-in;
}

.groupLink {
  display: flex;
  flex-direction: row;
  font-size: 20px;
  color: #fff;
}

.Link {
  margin-right: 85px;
  transition: transform 0.1s ease-in;
}

.pageName:hover {
  transform: scale(1.1);
  color: #ffff00;
  text-shadow: 1px -1px 0 #4000a7, -1px 2px 1px #4000a7;
  animation-name: spinTitle;
  animation-duration: 3s;
  animation-iteration-count: infinite;
}

.Link:hover {
  transform: scale(1.1);
  text-shadow: 1px -1px 0 #4000a7, -1px 2px 1px #4000a7;
}

@keyframes spinTitle {
  from {
    transform: rotate3d(1, 0, 0, 0deg);
  }

  to {
    transform: rotate3d(1, 0, 0, 359deg);
  }
}
  • 헤더 Hover시 애니메이션이 적용되도록 함. (계속 회전하도록)
    -> translate3d

3-1) Slide.js

import styles from './Slide.module.css';
import { useCallback, useEffect, useState } from 'react';
import MovieSlide from './MovieSlide';
import {
  IoIosArrowDropleftCircle,
  IoIosArrowDroprightCircle,
} from 'react-icons/io';
import Load from '../components/Load';

// Home
const Slide = ({ movieApi }) => {
  const [loading, setLoading] = useState(true);
  const [movies, setMovies] = useState([]);
  const [trans, setTrans] = useState(0);

  const onClickLeft = () => {
    if (trans >= 0) {
      // trans가 0 이상이면
      return;
    }
    setTrans((prevTrans) => prevTrans + 460);
    // 한 영화 슬라이드의 width가 460px임
  };

  const onClickRight = () => {
    if (trans <= -1380) {
      // 460 * 3 = 1380
      return;
    }
    setTrans((prevTrans) => prevTrans - 460);
  };

  const getMovies = useCallback(async () => {
    setLoading(true);
    const json = await (await fetch(movieApi)).json();
    setMovies(json.data.movies);
    setLoading(false);
  }, [movieApi]);

  useEffect(() => {
    getMovies();
  }, [getMovies]);
  // 최초 한번만 API를 불러옴

  return (
    <div className={styles.container}>
      <div className={styles.slideShow}>
        {loading ? (
          <Load />
        ) : (
          <div
            className={styles.slides}
            style={{ transform: `translateX(${trans}px)` }}
          >
            {/* eslint-disable-next-line array-callback-return */}
            {movies.map((movie) => {
              if (movie.medium_cover_image != null) {
                return (
                  // MovieSlide is redering code with Slide!
                  <MovieSlide
                    key={movie.id}
                    id={movie.id}
                    coverImg={movie.medium_cover_image}
                    rating={movie.rating}
                    runtime={movie.runtime}
                    title={movie.title}
                  />
                );
              }
            })}
          </div>
        )}
      </div>

      {/* Left, Right 버튼 */}
      {loading ? null : (
        <div className={styles.buttons}>
          <button className={styles.left} onClick={onClickLeft}>
            <IoIosArrowDropleftCircle />
          </button>
          <button className={styles.right} onClick={onClickRight}>
            <IoIosArrowDroprightCircle />
          </button>
        </div>
      )}
    </div>
  );
};

export default Slide;

3-2) Slide.module.css

/* container, slideShow, slides,  buttons, left, right */

/* reset CSS */

button {
  border: none;
  background-color: transparent;
}

.container {
  position: relative;
}

/*  230 * 4 =  */
.slideShow {
  position: relative;
  width: 920px;
  height: 500px;
  margin: 0 auto;
  overflow: hidden;
}

.slides {
  padding-top: 30px;
  display: flex;
  flex-direction: row;
  transition: transform 0.4s ease-in-out;
}

.left,
.right {
  color: #fff;
  font-size: 60px;
  cursor: pointer;
  transition: opacity 0.1s ease-in;
  filter: drop-shadow(2px 2px #3f00a7);
}

.buttons button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

.left {
  left: 130px;
}

.right {
  right: 130px;
}

.left:hover,
.right:hover {
  opacity: 0.6;
}
  • 지난시간 설명한 대로, 슬라이드 효과의원리를 이용하여 width를 잘 계산해서 적용함.

-> 참고로, translateX는 JSX에서 인라인 style로 넣어줬음. (변수로 계산해야 하므로)

  • 나중에 hover시 각 영화 슬라이드가 위로 올라가게 하기 위해, .slides 에다가 padding-top을 설정해줬음.

4-1) MovieSlide.js

import styles from './MovieSlide.module.css';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

const MovieSlide = ({ id, coverImg, rating, runtime, title }) => {
  return (
    <div className={styles.movie}>
      <div className={styles.coverImg}>
        <Link to={`/movie/${id}`}>
          <img src={coverImg} alt={title} />
        </Link>
      </div>

      <div className={styles.text}>
        <div className={styles.title}>
          <h3>
            <Link to={`/movie/${id}`}>
              {title.length > 35 ? `${title.slice(0, 35)}...` : title}
            </Link>
          </h3>
        </div>
        <div className={styles.info}>
          <span>{rating && `rating: ${rating} / 10`}</span>
          <span>{runtime && `runtime: ${runtime} min`}</span>
        </div>
      </div>
    </div>
  );
};

MovieSlide.propTypes = {
  id: PropTypes.number.isRequired,
  coverImg: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
};

export default MovieSlide;

4-2) MovieSlide.module.css

/*
movie coverImg text title info
*/

.movie {
  width: 230px;
  height: 500px;
  background-color: rgba(255, 255, 255, 0.7);
  border-radius: 6px;
  transition: all 0.2s ease-in;
  border-right: 2px solid rgba(0, 0, 0, 0.2);
}

.movie:hover {
  transform: translateY(-15px);
}

.coverImg img {
  border-radius: 6px 6px 0 0;
}

.text {
  padding: 3px 4px;
}

.title {
  text-align: center;
  font-size: 16px;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin-bottom: 3px;
  transition: all 0.1s ease-in;
}

.title:hover {
  color: #4000a7;
  text-decoration: underline;
  transform: scale(1.03);
}

.info {
  font-size: 13px;
  display: flex;
  flex-direction: column;
  color: #666;
  text-align: center;
}
  • 마찬가지로 hover에 트랜지션을 줘서 자연스럽게.

5-1) MovieGroup.js

import styles from './MovieGroup.module.css';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

const MovieGroup = ({
  id,
  coverImg,
  title,
  rating,
  runtime,
  year,
  summary,
}) => {
  return (
    <div className={styles.movie}>
      <div className={styles.layout}>
        <div className={styles.shortview}>
          <div className={styles.shortviewImg}>
            <img src={coverImg} alt={title} />
          </div>
          <div className={styles.shortviewText}>
            <div className={styles.shortviewTitle}>
              <h3>
                <Link to={`/movie/${id}`}>
                  {title.length > 35 ? `${title.slice(0, 35)}...` : title}
                </Link>
              </h3>
            </div>
            <div className={styles.shortviewInfo}>
              <div className={styles.year}>{year && `year: ${year}`}</div>
              <div className={styles.rating}>
                {rating && `rating: ${rating} / 10`}
              </div>
              <div className={styles.runtime}>
                {runtime && `runtime: ${runtime} min`}
              </div>
              <div className={styles.desc}>
                {summary
                  ? summary.length > 140
                    ? `${summary.slice(0, 140)}...`
                    : summary
                  : 'no summary data'}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

MovieGroup.propTypes = {
  id: PropTypes.number.isRequired,
  img: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  rating: PropTypes.number,
  runtime: PropTypes.number,
  summary: PropTypes.string,
};

export default MovieGroup;

5-2) MovieGroup.module.css

/*
movie(Container) shortview  shortviewImg shortviewText shortviewTitle 
shortviewInfo  -> year, rating, runtime, desc
*/

.movie {
  color: #111111;
  display: flex;
  justify-content: center;
}

.layout {
  display: flex;
  flex-direction: row;
  margin-right: 30px;
  margin-bottom: 30px;
}

.shortview {
  display: flex;
  flex-direction: row;
  margin: 10px;
  padding: 10px;
  width: 480px;
  height: 370px;
  background-color: rgba(255, 255, 255, 0.8);
  border-radius: 20px;
  overflow: hidden;
  transition: all 0.2s ease-in;
}

.shortview:hover {
  transform: translateY(-5px);
  box-shadow: 4px 8px 6px 0px rgba(0, 0, 0, 0.3);
}
.shortviewImg {
  margin-right: 20px;
}

.shortviewImg img {
  display: block;
  width: 230px;
  height: 345px;
  border-radius: 10px;
}

.shortviewText {
  width: 250px;
  height: 350px;
  padding: 10px;
}

.shortviewTitle {
  font-size: 18px;
  margin-bottom: 6px;
  transition: all 0.1s ease-in;
}

.shortviewTitle:hover {
  text-decoration: underline;
  transform: scale(1.05);
}

.year,
.rating {
  margin-bottom: 3px;
}

.runtime {
  margin-bottom: 8px;
}

.desc {
  font-size: 15px;
  color: #333333;
  letter-spacing: 0.01em;
}

6-1) MovieDetail.js

import styles from './MovieDetail.module.css';
import PropTypes from 'prop-types';

const MovieDetail = ({
  background_image_original,
  coverImg,
  rating,
  runtime,
  description_full,
  title,
  genres,
}) => {
  return (
    <div className={styles.movie}>
      <div className={styles.background}>
        <img src={background_image_original} alt="background" />
      </div>
      <div className={styles.shortview}>
        <div className={styles.shortviewImg}>
          <img src={coverImg} alt={title} />
        </div>
        <div className={styles.shortviewText}>
          <div className={styles.titleInfo}>
            <h3 className={styles.shortviewTitle}>{title}</h3>
            <div className={styles.shortviewInfo}>
              {rating && (
                <div className={styles.rating}>{`rating: ${rating} / 10`}</div>
              )}
              {runtime && (
                <div
                  className={styles.runtime}
                >{`runtime: ${runtime} min`}</div>
              )}
              {genres && (
                <div className={styles.genres}>
                  <h4>genres</h4>
                  <ul>
                    {genres.map((g) => (
                      <li key={g}>{g}</li>
                    ))}
                  </ul>
                </div>
              )}
            </div>
            <div className={styles.summary}>
              {description_full &&
                (description_full.length > 450 ? (
                  <div className={styles.desc}>
                    <p>{`${description_full.slice(0, 450)} ...`}</p>
                  </div>
                ) : (
                  <div className={styles.desc}>
                    <p>{description_full}</p>
                  </div>
                ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

MovieDetail.propTypes = {
  rating: PropTypes.number,
  runtime: PropTypes.number,
  coverImg: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  description_full: PropTypes.string,
  genres: PropTypes.arrayOf(PropTypes.string).isRequired,
};

export default MovieDetail;

6-2) MovieDetail.module.css

/* movie background 
Short view shortviewImg shortviewText shortviewTitle
shortviewInfo 
rating runtime genres desc*/

.movie {
  color: #fff;
}

.background {
  z-index: -1;
}

.background img {
  display: block;
  width: 100%;
  height: 100%;
  filter: brightness(50%);
}

.shortview {
  position: absolute;
  top: 35%;
  left: 15%;
  z-index: 2;
  display: flex;
}

.shortviewImg {
  margin-right: 50px;
}

.shortviewImg img {
  display: block;
  width: 300px;
}

.shortviewText {
  width: 60%;
}

.shortviewTitle {
  font-size: 38px;
  letter-spacing: 0.01em;
  margin-bottom: 16px;
}

.rating {
  color: #ffff00;
  font-size: 20px;
  margin-bottom: 8px;
}

.runtime {
  font-size: 20px;
  margin-bottom: 16px;
}

.genres {
  background-color: rgba(255, 255, 255, 0.2);
  padding: 10px 12px;
  border-radius: 20px;
  margin-bottom: 36px;
  display: flex;
  flex-direction: row;
}

.genres h4 {
  font-size: 20px;
  margin: 0 16px;
  text-shadow: 2px 2px #666;
}

.genres h4::before {
  content: ' ';
}

.genres ul {
  padding: 6px;
  display: flex;
  flex-direction: row;
}

.genres li {
  display: block;
  margin-left: 20px;
  font-size: 14px;
}
.genres ul:last-child {
  margin-right: 0;
}

.desc {
  font-size: 18px;
  letter-spacing: -0.01em;
}

.desc::before {
  content: '📝 summary';
  display: block;
  background-color: #fff;
  color: #666;
  width: 20%;
  text-align: center;
  padding: 3px 2px;
  border-radius: 6px;
  margin-bottom: 8px;
}

Route

7-1) Home.js

import styles from './Home.module.css';
import { Link } from 'react-router-dom';
import Slide from '../components/Slide';
import { ImPlay } from 'react-icons/im';
import { Group_obj, Group_key_arr } from '../atom/NavList';

const Home = () => {
  return (
    <div className={styles.container}>
      {Group_key_arr.map((group) => (
        <div key={group} className={styles.slideBox}>
          <div className={styles.title}>
            <Link
              to={`/page/${Group_obj[group]}/1`}
              style={{
                display: 'flex',
                flexDirection: 'row',
                alignContent: 'center',
                justifyContent: 'center',
              }}
            >
              <ImPlay />
              <h3>{group}</h3>
            </Link>
          </div>
          <Slide
            movieApi={`https://yts.mx/api/v2/list_movies.json?limit=10&${Group_obj[group]}&sort_by=rating`}
          />
        </div>
      ))}
      <div className={styles.footer}>
        <div className={styles.author}>
          <h4>thisisyjin</h4>
        </div>
        <ul className={styles.links}>
          <li>
            <a href="https://github.com/thisisyjin">githubs</a>
          </li>
          <li>
            <a href="https://mywebproject.tistory.com">dev log</a>
          </li>
          <li>
            <a href="mailto:thisisyjin@naver.com">contact</a>
          </li>
        </ul>
      </div>
    </div>
  );
};

export default Home;

7-2) Home.module.css

/*
container slideBox title 
footer author links
*/

.title {
  margin-top: 40px;
  transition: all 0.2s ease-in;
}

.title h3 {
  font-size: 42px;
  letter-spacing: 0.03em;
}

.title:hover {
  text-decoration: underline;
  color: #ff0000;
}

.title svg {
  margin-right: 5px;
}

.footer {
  margin-top: 100px;
  width: 100%;
  height: 65px;
  background-color: rgb(255, 255, 255, 0.8);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.author {
  margin-left: 80px;
  font-size: 26px;
  letter-spacing: 0.05em;
}

.author h4 {
  background: linear-gradient(
    to right,
    rgba(237, 193, 218, 1) 0%,
    rgba(136, 124, 211, 1) 50%,
    rgba(117, 174, 242, 1) 100%
  );
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  -webkit-background-clip: text;
}

.links {
  display: flex;
  margin-right: 80px;
}

.links li {
  color: #111;
  font-size: 20px;
  list-style: none;
  margin-left: 70px;
  transition: all 0.2s ease;
}

.links li:hover {
  color: #4000a7;
  transform: scale(1.1);
}

8-1) Group.js

import styles from './Group.module.css';
import { useEffect, useState, useCallback } from 'react';
import { useParams, Link } from 'react-router-dom';
import MovieGroup from '../components/MovieGroup';
import Load from '../components/Load';

const List_arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const Group = () => {
  const { group, page } = useParams();
  const [loading, setLoading] = useState(true);
  const [movies, setMovies] = useState([]);

  const getMovies = useCallback(async () => {
    setLoading(true);
    const json = await (
      await fetch(
        `https://yts.mx/api/v2/list_movies.json?page=${page}&${group}&sort_by=rating`
      )
    ).json();
    setMovies(json.data.movies);
    setLoading(false);
  }, [group, page]);

  useEffect(() => {
    getMovies();
    return;
  }, [getMovies]); // group이나 page 바뀔때마다

  return (
    <div className={styles.container}>
      {loading ? (
        <Load />
      ) : (
        <div className={styles.movies}>
          {movies.map((movie) => (
            <MovieGroup
              key={movie.id}
              id={movie.id}
              title={movie.title}
              coverImg={movie.medium_cover_image}
              rating={movie.rating}
              runtime={movie.runtime}
              summary={movie.summary}
              year={movie.year}
            />
          ))}
        </div>
      )}
      {loading ? null : (
        <div className={styles.footer}>
          <div className={styles.list}>
            {List_arr.map((lst) => (
              <Link key={lst} to={`/page/${group}/${lst}`}>
                {lst}
              </Link>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

export default Group;

8-2) Group.module.css

/*
container movies footer list 
*/

.container {
  margin-top: 40px;
  margin: 40px 20px;
}

.movies {
  width: 100%;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}

.footer {
  margin-top: 40px;
  color: #fff;
  background-color: rgba(255, 255, 255, 0.1);
  height: 60px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.list a {
  font-size: 24px;
  margin-left: 50px;
  transition: all 0.1s ease;
}

.list a:hover {
  color: #4000a7;
}

9-1) Detail.js

import styles from './Detail.module.css';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import MovieDetail from '../components/MovieDetail';
import Load from '../components/Load';

const Detail = () => {
  const { id } = useParams();
  const [loading, setLoading] = useState(true);
  const [movie, setMovie] = useState([]);

  const getMovie = useCallback(async () => {
    setLoading(true);
    const json = await (
      await fetch(`https://yts.mx/api/v2/movie_details.json?movie_id=${id}`)
    ).json();
    setLoading(false);
    setMovie(json.data.movie);
  }, [id]);

  useEffect(() => {
    getMovie();
  }, [getMovie]);

  return (
    <div className={styles.container}>
      {loading ? (
        <Load />
      ) : (
        <MovieDetail
          key={movie.id}
          id={movie.id}
          coverImg={movie.medium_cover_image}
          rating={movie.rating}
          runtime={movie.runtime}
          description_full={movie.description_full}
          background_image_original={movie.background_image_original}
          title={movie.title}
          genres={movie.genres}
        />
      )}
    </div>
  );
};

export default Detail;

9-2) Detail.module.css

.container {
  z-index: 1;
}

미리보기

  • 용량이 커서 low quality임.

후기 + 배운 점

우선 상태관리를 위해 recoil 라이브러리를 썼는데, 이게 맞나 싶다.
그냥 모듈로 쓴것 같아서 뭔가 찝찝하다.
우선 초기 버전일 뿐이고, 계속 수정해갈 예정이다.

그리고 중간에 Load 컴포넌트가 계속해서 렌더링이 안되는 에러가 발생했었다.
내가 getMovies()가 비동기 async/await 함수임을 까먹고 먼저 setLoading(false)를 해버려서
그런 것이였다.

차분히 console.log 찍어보고 다행히 해결했다. good👍

또, getMovies 함수를 useCallback으로 감싸주고 useEffect에 넣어줘야 에러가 안발생한다.
es-lint는 너무 엄격해서 화난다. (..?)


추가할 것

  1. 더 화려한 CSS 효과 + UI 개선

  2. recoil 라이브러리 기능 추가

  3. search 페이지 추가
    -> 사실 day01에서 서치 기능을 추가하려 했으나,
    우선 ui적으로도 헤더에 자리가 없기도 하고 해서 제외시켰다.

나중에 서치 기능도 추가할 예정. (이것도 useParams로 구현 가능)

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글