본문 바로가기
영화 검색 사이트

[영화 검색 사이트] 영화 상세 페이지(스켈레톤 UI, Loader)

by mikrw 2023. 2. 11.

스켈레톤 UI

실제 데이터가 렌더링 되기 전에 보이게 될 화면의 윤곽을 먼저 그려주는 로딩 애니메이션

사용자의 이탈을 막고, ‘어떤 것들이 보여질 것이다’라고 미리 알려주는 효과

 


Loader.vue

<template>
  <div
    :style="{
      width: `${size}rem`,
      height: `${size}rem`,
      zIndex
    }"
    :class="{ absolute, fixed }"
    class="spinner-border text-primary"></div>
</template>

<script>
  export default {
    props: {
      size: {
        type: Number,
        default: 2
      },
      absolute: {
        type: Boolean,
        default: false
      },
      fixed: {
        type: Boolean,
        default: false
      },
      zIndex: {
        type: Number,
        default: 0
      }
    }
  }
</script>

<style lang="scss" scoped>
  .spinner-border {
    margin: auto;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    &.absolute {
      position: absolute;
    }
    &.fixed {
      position: fixed;
    }
  }
</style>

Movie.vue

<template>
  <div class="container">
    <template v-if="loading">
      <div class="skeletons">
        <div class="skeleton poster"></div>
        <div class="specs">
          <div class="skeleton title"></div>
          <div class="skeleton spec"></div>
          <div class="skeleton plot"></div>
          <div class="skeleton etc"></div>
          <div class="skeleton etc"></div>
          <div class="skeleton etc"></div>
        </div>
      </div>

      <Loader
        :size="3"
        :z-index="9"
        fixed />
    </template>

    <div
      v-else
      class="movie-details">
      <div
        :style="{ backgroundImage: `url(${requestDiffSizeImage(theMovie.Poster)})` }"
        class="poster"></div>
      <div class="specs">
        <div class="title">
          {{ theMovie.Title }}
        </div>
        <div class="labels">
          {{ theMovie.Released }}
          {{ theMovie.Runtime }}
          {{ theMovie.Country }}
        </div>
        <div class="plot">
          {{ theMovie.Plot }}
        </div>
        <div class="ratings">
          <h3>Ratings</h3>
          <div class="rating-wrap">
            <div class="rating">
              <div
                v-for="{ Source: name, Value: score } in theMovie.Ratings"
                :key="name"
                :title="name"
                class="rating">
                <img
                  :src="`https://raw.githubusercontent.com/ParkYoungWoong/vue3-movie-app/master/src/assets/${name}.png`"
                  :alt="name" />
                <span>{{ score }}</span>
              </div>
            </div>
          </div>
        </div>
        <div>
          <h3>Actors</h3>
          {{ theMovie.Actors }}
        </div>
        <div>
          <h3>Director</h3>
          {{ theMovie.Director }}
        </div>
        <div>
          <h3>Production</h3>
          {{ theMovie.Production }}
        </div>
        <div>
          <h3>Genre</h3>
          {{ theMovie.Genre }}
        </div>
      </div>      
    </div>
  </div>
</template>

<script>
  import Loader from '~/components/Loader';

  export default {
    components: {
      Loader
    },
    computed: {
      theMovie() {
        return this.$store.state.movie.theMovie
      },
      loading() {
        return this.$store.state.movie.loading
      }
    },
    created() {
      this.$store.dispatch('movie/searchMovieWithId', {
        id: this.$route.params.id
      })
    },
    methods: {
      requestDiffSizeImage(url, size = 700) {
        return url.replace('SX300', `SX${size}`)
      }
    }
  }
</script>

<style lang="scss" scoped>
  @import '~/scss/main';

  .container {
    padding-top: 40px;
  }
  .skeletons {
    display: flex;
    .poster {
      flex-shrink: 0;
      width: 500px;
      height: 500px * calc(3 / 2);
      margin-right: 70px;
    }
    .specs {
      flex-grow: 1;
    }
    .skeleton {
      border-radius: 10px;
      background-color: $gray-200;
      &.title {
        width: 80%;
        height: 70px;
      }
      &.spec {
        width: 60%;
        height: 30px;
        margin-top: 20px;
      }
      &.plot {
        width: 100%;
        height: 250px;
        margin-top: 20px;
      }
      &.etc {
        width: 50%;
        height: 50px;
        margin-top: 20px;
      }
    }
  }

  .movie-details {
    display: flex;
    color: $gray-600;
    .poster {
      flex-shrink: 0;
      width: 500px;
      height: 500px * calc(3 / 2);
      margin-right: 70px;
      border-radius: 10px;
      background-color: $gray-200;
      background-size: cover;
      background-position: center;
    }
    .specs {
      flex-grow: 1;
      .title {
        color: $black;
        font-family: 'Oswald', sans-serif;
        font-size: 70px;
        line-height: 1;
        margin-bottom: 30px;
      }
      .labels {
        color: $primary;
        span {
          &::after {
            content: "\00b7";
            margin: 0 6px;
          }
          &:last-child::after {
            display: none;
          }
        }
      }
      .plot {
        margin-top: 20px;
      }
      .ratings {
        .rating-wrap {
          display: flex;
          .rating {
            display: flex;
            align-items: center;
            margin-right: 32px;
            img {
              height: 30px;
              flex-shrink: 0;
              margin-right: 6px;
            }
          }
        }
      }
      h3 {
        margin: 24px 0 6px;
        color: $black;
        font-family: "Oswald", sans-serif;
        font-size: 20px;
      }
    }
  }
</style>