Merhaba,

 

Bu yazıda Vue.js ve Movie API ile Film Uygulaması yapımından bahsedeceğim. Uygulamada filmlerin listelendiği bir ana ekranımız ve film detay ekranımız olacak. State yönetimi için de Vuex kullanacağız.

 

İlk olarak bir Vue.js projesi oluşturduğumuzu varsayıyorum. Öncelikle ihtiyacımız olacak olan paketleri kuruyoruz:

npm i vue-awesome-swiper vue-resource vue-router vuex
  • vue-awesome-swiper: Filmlerin listelendiği ana ekranda bir slider yapacağız. Bu paketi de onun için kullanacağız.
  • vue-resource: Http isteklerini yaparken bu paketten faydalanacağız.
  • vue-router: Route tanımlamalarını bu paketle yapacağız.
  • vuex: State yönetimini bu paketle sağlayacağız.

 

Projemizde ayrıca Scss kullanacağız. Bunun için de gerekli paketleri kuruyoruz:

npm i sass sass-loader --save-dev

 

Şimdi de projemizin klasör yapısını ayarlayalım. src klasörü içinde components, config ve store adında klasörler oluşturuyoruz. Sonrasında components içinde pages ve shared adında klasörler oluşturuyoruz. Burada pages içinde projede bulunacak ekranlarımızı, shared içinde ise ortak kullanılacak olan componentleri tasarlayacağız. config klasörü içinde env.js dosyası açarak burada API bilgilerini, store klasörü içinde ise state yönetimiyle ilgili dosyaları açarak burada gerekli tanımlamaları yapacağız. Ayrıca yine src dizininde router.js dosyası oluşturarak burada route tanımlamalarını gerçekleştireceğiz. Klasör yapımızın son hali şu şekilde olacak:

 

Şimdi router.js dosyasını açarak gerekli route tanımlamalarını yapıyoruz:

router.js:

import Vue from "vue"
import VueRouter from "vue-router"

import Home from "./components/pages/Home"
import About from "./components/pages/About"
import Detail from "./components/pages/Detail"

Vue.use(VueRouter)

const routes = [
  {
    path: "/",
    component: Home
  },
  {
    path: "/about",
    component: About
  },
  {
    path: "/detail/:id",
    component: Detail
  }
]

export const router = new VueRouter({
  mode: "history",
  routes
})

 

config/env.js dosyamızı açarak Movie API'dan aldığımız bilgileri giriyoruz:

env.js:

export const API_URL = "https://api.themoviedb.org/3"
export const API_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2NWNkMjAwZDFkNWMyOTE1NGIwNTk0YjZkZTllYTA3MSIsInN1YiI6IjVhYTdkYjcyOTI1MTQxNWUzOTAxZGUyMiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.n4H4Ag0mQxg-530d1wFbfUkd_OIiPuUP59OwCDQpH6A"

Not: Token bilgisini açık olarak paylaşmamın bir sakıncası bulunmuyor çünkü API ücretsiz :) Ancak normal şartlarda bu tarz bilgileri açık olarak tutmamak daha iyi olacaktır.

 

store/index.js dosyamızı açarak Vuex tanımlamalarını yapıyoruz:

import Vue from "vue"
import Vuex from "vuex"
import * as actions from "./actions"
import * as getters from "./getters"
import * as mutations from "./mutations"

Vue.use(Vuex)

export const store = new Vuex.Store({
  state: {
    popularMovies: [],
    trendMovies: [],
    movieDetail: {},
    cast: {}
  },
  actions,
  getters,
  mutations
})
  • Anasayfada popüler ve trend olan filmleri göstereceğiz. Bunları popularMovies ve trendMovies şeklinde state'de tutuyoruz.
  • Film detay ekranında filmle ilgili daha detaylı bilgiler ve oyuncu bilgilerini göstereceğiz. Bunları da movieDetail ve cast şeklinde state'de tutuyoruz.

 

store/actions.js dosyamızı açarak ihtiyacımız olacak API isteklerini tanımlıyoruz:

import Vue from "vue"
import { API_URL } from "../config/env"

export const popularMovies = ({ commit }) => {
  Vue.http.get(`${API_URL}/movie/popular`)
    .then(response => {
      commit("setPopularMovies", response.body.results)
    })
}

export const trendMovies = ({ commit }) => {
  Vue.http.get(`${API_URL}/movie/top_rated`)
    .then(response => {
      commit("setTrendMovies", response.body.results)
    })
}

export const movieDetail = ({ commit }, data) => {
  Vue.http.get(`${API_URL}/movie/${data.id}`)
    .then(response => {
      commit("setMovieDetail", response.body)
    })
}

export const cast = ({ commit }, data) => {
  Vue.http.get(`${API_URL}/movie/${data.id}/credits`)
    .then(response => {
      commit("setCast", response.body.cast)
    })
}
  • API istekleri sonrası gelen verileri state'de bulunan değişkenlerimize aktarmak için mutation'lara commit ediyoruz.

 

store/mutations.js dosyamızı açarak state güncellemeleri yapacak olan fonksiyonları tanımlıyoruz:


export const setPopularMovies = (state, popularMovies) => {
  state.popularMovies = popularMovies
}

export const setTrendMovies = (state, trendMovies) => {
  state.trendMovies = trendMovies
}

export const setMovieDetail = (state, movieDetail) => {
  state.movieDetail = movieDetail
}

export const setCast = (state, cast) => {
  state.cast = cast
}

 

store/getters.js dosyamızı açarak state'te bulunan değişkenlerimize ulaşacak fonksiyonlarımızı tanımlıyoruz:

export const getPopularMovies = (state) => {
  return state.popularMovies
}

export const getTrendMovies = (state) => {
  return state.trendMovies
}

export const getMovieDetail = (state) => {
  return state.movieDetail
}

export const getCast = (state) => {
  return state.cast
}

 

main.js dosyamızı açarak proje genelinde ihtiyacımız olacak tanımlamaları yapıyoruz:

import Vue from 'vue'
import App from './App.vue'

import { router } from "./router"
import { store } from "./store"
import VueResource from "vue-resource"

import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/swiper-bundle.css'

import { API_TOKEN } from "./config/env"

Vue.use(VueResource)
Vue.use(VueAwesomeSwiper)

Vue.http.interceptors.push((request) => {
  request.headers.set('Authorization', 'Bearer ' + API_TOKEN)
})

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')
  • router, store, vue-resource ve vue-awesome-swiper'ı dahil ettik.
  • API_TOKEN bilgisini dahil ettik.
  • Bir interceptor tanımlayarak yapılan tüm Http isteklerinde header kısmına Authorization: Bearer {token} bilgisinin eklenmesini sağladık.

 

Artık component'lerimize geçebiliriz;

Header.vue:

<script>
export default {
    
}
</script>

<template>
  <header>
    <div class="container">
      <div class="logo">
        <router-link to="/" tag="a">
          <img src="../../assets/logo.png" />
        </router-link>        
      </div>
      <div class="menu">
        <router-link to="/" tag="a">Home</router-link>
        <router-link to="/about" tag="a">About</router-link>
      </div>
    </div>    
  </header>
</template>

<style lang="scss" scoped>
header {
  height: 7rem;
  background: #34495e;
  display: flex;
  justify-content: center;

  .container {
    width: 60%;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .logo {
      cursor: pointer;
      height: 4rem;

      img {
        width: 4rem;
      }
    }

    .menu {
      a {
        color: #ffffff;
        margin: 0 5px;
        padding: 1rem;
        font-size: 1.2rem;

        &.router-link-exact-active {
          border: 1px solid #54677a;
          background: #54677a;
          border-radius: 5px;
        }
      }
    }
  }
}
</style>

 

Footer.vue:

<script>
export default {
    
}
</script>

<template>
  <footer>
    <span>Movie App @ 2022</span>
  </footer>
</template>

<style lang="scss" scoped>
footer {
  height: 7rem;
  width: 100%;
  background: #34495e;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1rem;

  span {
    color: #ffffff;
  }
}
</style>

 

Slider.vue:

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'

export default {
  props: ['title', 'movies'],
  components: {
    Swiper,
    SwiperSlide
  },
  data() {
    return {      
      swiperOptions: {
        slidesPerView: 3,
        mousewheel: true,
        spaceBetween: 30,
        autoplay: {
          delay: 2500,
          disableOnInteraction: false
        },
        loop: true,
        loopFillGroupWithBlank: true,
        breakpoints: {
          1024: {
            slidesPerView: 5,
            spaceBetween: 10
          },
          768: {
            slidesPerView: 3,
            spaceBetween: 10
          },
          640: {
            slidesPerView: 2,
            spaceBetween: 10
          },
          320: {
            slidesPerView: 1,
            spaceBetween: 10
          }
        }
      }
    }
  }
}
</script>

<template>
  <section>
    <h2>{{ title }}</h2>
    <swiper 
      class="swiper"
      :options="swiperOptions">
      <swiper-slide
        v-bind:key="item.id"
        v-for="item in movies">
        <router-link :to="{ path: 'detail/' + item.id }" tag="a">
          <div class="movie-item">
            <div class="top-info">
              <span class="rate">{{ item.vote_average }}</span>
              <span class="date">{{ item.release_date.substr(0, 4) }}</span>
            </div>              
            <img :src="`https://www.themoviedb.org/t/p/w300_and_h450_bestv2${item.poster_path}`" class="poster" />
            <span class="title">{{ item.title }}</span>              
          </div>          
        </router-link>
      </swiper-slide>
    </swiper>  
  </section>
</template>

<style lang="scss" scoped>
section {

  h2 {
    margin-top: 1rem;
  }

  .swiper {
    margin-top: 1rem;
  }

  .movie-item {
    display: flex;
    flex-direction: column;
    align-items: center;

    .top-info {
      display: flex;
      width: 100%;
      justify-content: space-between;

      .rate {
        background: #27ae60;
        color: white;
        padding: 3px;
        border-radius: 3px 3px 0 0;
      }

      .date {
        background: #34495e;
        color: white;
        padding: 3px;
        font-size: 13px;
        border-radius: 3px 3px 0 0;
      }
    }
    
    .poster {
      width: 100%;
    }
    
    .title {
      color: black;
      font-weight: bold;
    }

  }

}
</style>
  • Slider component'ini anasayfada hem popüler hem de trend filmler kısmında kullanacağız. O açıdan burada title ve movies bilgisini props olarak aldık.
  • swiperOptions ile swiper ayarlarını tanımladık. Bu ayarların neye karşılık geldiğiyle ilgili olarak buraya göz atabilirsiniz.

 

Home.vue:

<script>
import { mapGetters } from "vuex"

import Slider from "../shared/Slider.vue"

export default {
  created() {
    this.$store.dispatch('popularMovies')
    this.$store.dispatch('trendMovies')
  },
  components: {
    Slider
  },
  computed: {
    ...mapGetters(["getPopularMovies", "getTrendMovies"])
  }
}
</script>

<template>
  <div class="container">
    <div class="home">
      <section class="jumbotron">
        <h1>Movie App</h1>
      </section>
      <Slider title="Popular Movies" :movies="getPopularMovies" />
      <Slider title="Trend Movies" :movies="getTrendMovies" />
    </div>    
  </div>  
</template>

<style lang="scss" scoped>
.container {  
  display: flex;
  justify-content: center;

  .home { 
    width: 60%;

    .jumbotron {
      padding: 5rem;
      background: #ecf0f1;
      margin: 1rem 0;
      border-radius: 5px;
    }
  }
}
</style>
  • Ekran yüklendiğinde (created) store'da bulunan popularMovies ve trendMovies action'larını dispatch ettik.
  • computed kısmında ihtiyacımız olan getter'lara ulaştık.
  • Elde ettiğimiz verileri Slider component'imize props olarak gönderdik.

 

About.vue:

<script>
export default {
    
}
</script>

<template>
  <section>
    <span>About</span>
  </section>
</template>

<style lang="scss" scoped>
section {
  display: flex;
  align-items: center;
  text-align: center;
  height: 5rem;
  justify-content: center;

  span {
    font-size: 2rem;
  }
}
</style>
  • Bu sayfanın tek amacı Header'da bulunan menüde ekstradan bir buton görünmesiydi :)

 

Detail.vue:

<script>
import { mapGetters } from "vuex"

export default {
  created() {
    this.$store.dispatch("movieDetail", { id: this.$route.params.id })
    this.$store.dispatch("cast", { id: this.$route.params.id })
  },
  computed: {
    ...mapGetters(["getMovieDetail", "getCast"])
  }
}
</script>

<template>
  <div class="container">
    <div class="detail">      
      <div class="top-infos">
        <img :src="`https://www.themoviedb.org/t/p/w300_and_h450_bestv2${getMovieDetail.poster_path}`" />
        <div class="texts">
          <div class="general">
            <h1>{{ getMovieDetail.title }}</h1>
            <span class="rate">{{ getMovieDetail.vote_average }}</span>
            <span class="date">{{ getMovieDetail.release_date.substr(0, 4) }}</span>
          </div>          
          <ul class="categories">
            <li v-for="genre in getMovieDetail.genres" v-bind:key="genre.id">{{ genre.name }}</li>
          </ul>
          <span>{{ getMovieDetail.overview }}</span>
        </div>        
      </div>      
      <h2>Cast</h2>
      <ul class="cast">
        <li v-for="item in getCast" v-bind:key="item.id">
          <img :src="item.profile_path !== null ? `https://www.themoviedb.org/t/p/w300_and_h450_bestv2${item.profile_path}` : `https://via.placeholder.com/100x150`" />
          <span class="name">{{ item.name }}</span>
          <span class="title">{{ item.character }}</span>
        </li>
      </ul>
    </div>    
  </div>
</template>

<style lang="scss" scoped>
.container {  
  display: flex;
  flex-direction: column;
  align-items: center;

  @media screen and (max-width: 400px) {
    flex-direction: row;
  }

  .detail { 
    width: 60%;

    .top-infos { 
      display: flex;
      margin: 1rem 0 1rem 0;

      @media screen and (max-width: 400px) {
        display: block;
      }   

      .texts { 
        margin-left: 1rem;

        .general { 
          display: flex;
          flex-direction: column;

          .rate { 
            width: fit-content;
            background: #27ae60;
            color: white;
            padding: 3px;
            border-radius: 3px 3px 0 0;
            margin-top: 1rem;
          }

          .date {
            width: fit-content;
            background: #34495e;
            color: white;
            padding: 3px;
            font-size: 13px;
            border-radius: 3px 3px 0 0;
            margin-top: 1rem;
          }

        }

        .categories {
          display: flex;
          margin-top: 1rem;
          margin-bottom: 1rem;

          li {
            margin-right: 1rem;
            background: #34495e;
            color: white;
            padding: 5px;
            border-radius: 5px;
            font-size: 13px;
          }

        }

      }
    }

    .cast { 
      display: flex;
      overflow: auto;

      li {
        display: flex;
        margin: 1rem 1rem 0 0;
        flex-direction: column;

        img {
          width: 100px;
        }

        .name {
          font-size: 14px;
        }

        .title {
          font-size: 11px;
        }

      }

    }

  }

}
</style>
  • Ekran yüklendiğinde (created) store'da bulunan movieDetail ve cast action'larını dispatch ettik.
  • computed kısmında ihtiyacımız olan getter'lara ulaştık.
  • Elde ettiğimiz verileri ekranda gösterdik.

 

Projenin ekran görüntülerine ve kodlarına buradan ulaşabilirsiniz. Ayrıca canlı halini de buradan görebilirsiniz.

 

Umarım yararlı olmuştur.

 

İyi çalışmalar.