Простое фото приложение на Vue.js, Axios и Flickr API — Часть 3

Spread the love

Part 1 | Part 2 | Part 3:

Ранее мы реорганизовали наше приложение, чтобы оно было более гибким и DRY. В этой статье мы добавим 3 новых компонента на домашнюю страницу, которая в настоящее время пуста:

  • Компонент HeroSection
  • Компонент RecentPhotos для отображения последних загруженных изображений с Flickr
  • Компонент PlacePhotos, позволяющий пользователям просматривать фотографии из списка мест.

Шаг 1: Компонент HeroSection

Начнем с компонента HeroSection.vue. Создадим файл с таким именем в каталоге компонентов. В нем будет следующее:

  • Шаблон будет состоять из одного элемента section, к которому будет применен backgroundImage. Внутри него будут title и subtitle. Эти элементы будут Strings, которые компонент получает в качестве props , чтобы мы могли теоретически снова использовать этот компонент на другой странице с другими images / titles / subtitles.
  • BackgroundImage будет установлен через атрибут :style. Обычные фоновые изображения CSS создаются с этим синтаксисом:
background-image: url('relative-or-external-path.jpg');
  • Но поскольку мы делаем это в шаблоне, мы должны использовать camel case стиль для имен стилей CSS (backgroundImage, а не background-image), и мы используем метод heroImagePath() для построения необходимого пути. heroImagePath() возвращает объединенный относительный путь к файлу, который мы будем хранить в каталоге assets нашего проекта. Поскольку мы используем Webpack для компиляции файлов, мы должны использовать метод require(), чтобы помочь Webpack найти требуемое изображение.
  • Так же мы добавили адаптивные стили CSS в конце файла.
<template>
  <section class="hero" :style="{backgroundImage: heroImagePath()}">
    <h1 class="hero-title">{{ title }}</h1>
    <p class="hero-subtitle">{{ subtitle }}</p>
  </section>
</template>

<script>
export default {
  name: 'heroSection',
  props: {
    backgroundImage: String,
    title: String,
    subtitle: String,
  },
  methods: {
    heroImagePath() {
      return 'url(' + require(`../assets/${this.backgroundImage}`) + ')';
    }
  }
}
</script>


<style lang="scss">
.hero {
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center center;
  color: white;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 600px;
  @media only screen and (min-width: 600px) and (max-width: 900px) {
    min-height: 400px;
  }
  @media only screen and (max-width: 599px) {
    min-height: 250px;
  }
}
.hero-title {
  font-size: 3rem;
  margin: 0;
}
.hero-subtitle {
  font-size: 1.3rem;
}
</style>

Теперь посетите Unsplash или Pixabay, и загрузите от туда бесплатную фотографию на ваш выбор в качестве фонового изображения. Сохраните его в assets как homepage-hero.jpg.

Затем перейдите в Home.vue и обновите его следующим образом:

<template>
  <div>
    <hero-section
      backgroundImage="homepage-hero.jpg"
      title="Welcome to Instaflickr"
      subtitle="A fake photo app"/>
    <div class="wrapper">
    </div>
  </div>
</template>

<script>
import HeroSection from '@/components/HeroSection';
export default {
  name: 'home',
  components: { 
    HeroSection,
  }
};
</script>

В нем мы импортируем и регистрируем компонент HeroSection. Затем мы добавляем его в шаблон, передавая необходимые реквизиты.

Шаг 2: Компонент RecentPhotos

Теперь давайте создадим раздел на главной странице, чтобы показать три самые последние фотографии, которые были загружены на Flickr. Мы будем использовать метод flickr.photos.getRecent, который потребует наш api_key и так де принимает много дополнительных параметров, аналогичных методу flickr.photos.search, таких как extras, per_page и page. Он отлично подойдет к нашему вспомогательному методу flickr.js, который мы сделали в части 2. А так же мы воспользуемся нашим компонентом ImageCard для отображения получаемых изображений.

Создайте файл с именем RecentPhotos.vue внутри папки компонентов. В нем мы сделаем следующее:

  • Шаблон будет состоять из title и ul из компонентов ImageCard (импортированных и зарегистрированных). Цикл v-for будет перебирать вычисляемое свойство mostRecentPhotos.
  • mostRecentPhotos будет возвращать первые три изображения в массиве с именем RecentPhotos, который будет хранится в data().
  • RecentPhotos инициализируется пустым массивом, который заполняется изображениями во время хука create().
  • В create() мы вызываем метод fetchRecentPhotos(), который использует наш вспомогательный метод flickr() для вызова API.
  • flickr() получает два параметра: метод, который мы хотим вызвать (photos.getRecent), и дополнительные параметры, которые ему нужны (extras, page и per_page). Если запрос метода успешно разрешен, мы обновляем latestPhotos в методе then(). Если есть ошибка, мы отобразим ее в консоли в методе catch().

Это будет наш стандартный подход/процесс для всех остальных вызовов API в этом проекте.

<template>
  <section class="content-section margin-y--6">
      <h2 class="centered">Browse the Latest Uploads</h2>
      <ul class="image-card-grid">
        <image-card
          v-for="image in mostRecentPhotos"
          :key="image.id"
          :image="image"
        />
      </ul>
    </section>
</template>

<script>
import flickr from '../flickr'
import ImageCard from './ImageCard.vue'

export default {
  name: 'recentPhotos',
  components: { ImageCard },
  created() {
    this.fetchRecentPhotos()
  },
  data() {
    return {
      recentPhotos: [],
    }
  },
  computed: {
    mostRecentPhotos() {
      return this.recentPhotos.slice(0, 3)
    },
  },
  methods: {
    fetchRecentPhotos() {
      return flickr("photos.getRecent", {
        extras: "url_n, owner_name, description, date_taken, views",
        page: 1,
        per_page: 3,
      })
        .then((response) => {
          this.recentPhotos = response.data.photos.photo
        })
        .catch((error) => {
          console.log("Error occured: ", error)
        })
    },
  }
}
</script>

<style>
.centered { text-align: center; }
.margin-y--6 {
  margin-top: 6rem;
  margin-bottom: 6rem;
}
</style>

Затем добавьте новый компонент в компонент Home.vue, импортировав и зарегистрировав его и, затем добавим тег в шаблон.

<template>
  <div>
    <hero-section
      backgroundImage="homepage-hero.jpg"
      title="Welcome to Instaflickr"
      subtitle="A fake photo app"/>
    <div class="wrapper">
      <recent-photos />
    </div>
  </div>
</template>

<script>
import HeroSection from '@/components/HeroSection';
import RecentPhotos from '@/components/RecentPhotos';

export default {
  name: 'home',
  components: { 
    HeroSection,
    RecentPhotos
  }
};
</script>

Шаг 3: Компонент PlacePhotos

Другой популярный способ поиска фотографий – по названию города или места. Давайте создадим интерактивный компонент, который позволяет пользователям просматривать фотографии из ограниченного списка «популярных» мест. Мы произвольно укажем места какие мы хотим видеть.

Создайте файл с именем PlacePhotos.vue внутри папки компонентов. Наш шаблон будет состоять из заголовка и список кнопок с названием мест. Каждая кнопка будет обновляет список из 3 фотографий с выбранного места; вторая строка – это loading который будет появляться во время выборки изображений. Внизу будет ссылка, по которой пользователь может щелкнуть, чтобы просмотреть другие фотографии этого места на странице SearchResults.

Разберем это еще раз и начнем с первой части, списка мест ul:

  • Цикл li проходит через PopularPlaces, массив в data(), который состоит из объектов place. Каждый объект имеет свойства name и place_id. Place_id происходит из метода flickr.places.find. Не стесняйтесь заменять эти имена и идентификаторами по вашему выбору!
  • Когда пользователь нажимает кнопку «place», вызывается метод updateSelectedPlaceIndex(index). updateSelectedPlaceIndex принимает один аргумент – индекс объекта place в PopularPlaces и обновляет свойство данных selectedPlaceIndex, чтобы сохранить новое значение. Как мы увидим через минуту, это является частью того, что заставляет фотографии обновляться.

Теперь давайте посмотрим на вторую часть, image-card-grid u:

  • Оно состоит из списка ImageCards, изображений которые получены из popularPlacePhotos , вычисляемого свойства, которое возвращает первые три изображения из placePhotos.
  • placePhotos аналогичен последним фотографиям из предыдущего компонента. Он инициализируется как пустой массив, а затем заполняется данными во время хука жизненного цикла create(), на этот раз с помощью метода fetchPlacePhotos().
  • fetchPlacePhotos() делает то же самое, что и fetchRecentPhotos(), только с другим методом API, photos.search.
  • Основное отличие в том что мы ищем по place_id, а не по тегу на этот раз. Мы знаем, какой place_id нужно передать, путем индексации в PopularPlaces, используя текущее значение selectedPlaceIndex, а затем передавая place_id этого места. Но вместо того, чтобы писать:
place_id: this.popularPlaces[this.selectedPlaceIndex].place_id

мы перемещаем первую часть в ее собственное вычисляемое свойство с именем selectedPlace , что позволяет нам написать так:

place_id: this.selectedPlace.place_id

Что гораздо более читабельно.

Наконец, давайте посмотрим на третью часть шаблона:

  • Он состоит из выровненного по центру тега <p>, который оборачивает элемент router-link (он же тег <a>).
  • Далее указывается имя маршрута name в searchResults и передает имя места в качестве значения для нашего поискового параметра name, тега tag.
<template>
  <section class="content-section margin-y--6">
    <h2 class="centered">Daydream by Place</h2>
    <ul class="place-list">
      <li 
        v-for="(place, index) in popularPlaces" 
        :key="place.place_id" 
        class="place-list__item">
        <button 
          @click="updateSelectedPlaceIndex(index)" 
          class="place-list__item__button" 
          :class="{'selected': index === selectedPlaceIndex}">
            {{place.name}}
        </button>
      </li>
    </ul>

    <ul v-if="!loading" class="image-card-grid">
      <image-card
        v-for="image in popularPlacePhotos"
        :key="image.id"
        :image="image"
      />
    </ul>
    <ul v-else class="image-card-grid">
      <image-card
        v-for="n in 3"
        :key="n"
        :loading="true"
        />
    </ul>

    <p class="centered">
      <router-link 
        :to="{name: 'searchResults', params: { tag: selectedPlace.name }}" 
        class="btn btn--dark-grey more-photos">
        More Photos of {{selectedPlace.name}}
      </router-link>
    </p>
  </section>
</template>

<script>
import flickr from '../flickr'
import ImageCard from './ImageCard.vue'
export default {
  name: 'daydreamByPlace',
  components: { ImageCard },
  created() {
    this.fetchPlacePhotos()
  },
  watch: {
    selectedPlace() {
      this.fetchPlacePhotos()
    }
  },
  data() {
    return {
      loading: true,
      placePhotos: [],
      selectedPlaceIndex: 0,
      popularPlaces: [
        {
          name: 'Agra',
          place_id: 'S4OOvxtTULO18fQG'
        },
        {
          name: 'Bali',
          place_id: 'lm4_wrhTUb4oe5pO'
        },
        {
          name: 'Rio de Janeiro',
          place_id: 'mAqmHW5VV78OT5o'
        },
        {
          name: 'Paris',
          place_id: 'EsIQUYZXU79_kEA'
        },
        {
          name: 'Tokyo',
          place_id: 'FRthiQZQU7uKHvmP'
        },
        {
          name: 'Tolanaro',
          place_id: 'qdHKy7VQUbxPQVBX'
        }
      ]
    }
  },
  computed: {
    selectedPlace() {
      return this.popularPlaces[this.selectedPlaceIndex]
    },
    popularPlacePhotos() {
      return this.placePhotos.slice(0, 3)
    },
  },
  methods: {
    fetchPlacePhotos() {
      this.loading = true
      flickr("photos.search", {
        place_id: this.selectedPlace.place_id,
        extras: "url_n, owner_name, description, date_taken, views",
        page: 1,
        per_page: 3,
      }).then((response) => {
          this.placePhotos = response.data.photos.photo
          this.loading = false
        })
        .catch((error) => {
          console.log("Error occured: ", error)
        })
    },
    updateSelectedPlaceIndex(index) {
      this.selectedPlaceIndex = index;
    },
  }
}
</script>

<style lang="scss">
.btn--dark-grey {
  background: #2c3e50;
  color: white;
  text-decoration: none;
}
.place-list {
  list-style: none;
  padding: 0;
  margin: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.place-list__item {
  margin: 0 .5rem;
}
.place-list__item__button {
  background: transparent;
  font-size: 1rem;
  border: 0;
  outline: 0;
  cursor: pointer;
  border-radius: 3px;
  padding: .5rem .75rem;
  &.selected {
    background: #F0F0F0;
  }
  transition: all .3s ease;
  &:hover {
    color: #42b983;
  }
}
</style>

Единственными оставшимися шагами теперь являются импорт и регистрация PlacePhotos.vue в Home.vue и добавление его в шаблон:

<template>
  <div>
    <hero-section
      backgroundImage="homepage-hero.jpg"
      title="Welcome to Instaflickr"
      subtitle="A fake photo app"/>
    <div class="wrapper">
      <recent-photos />
      <place-photos />
    </div>
  </div>
</template>

<script>
import HeroSection from '@/components/HeroSection';
import RecentPhotos from '@/components/RecentPhotos';
import PlacePhotos from '@/components/PlacePhotos';

export default {
  name: 'home',
  components: { 
    HeroSection,
    RecentPhotos,
    PlacePhotos
  }
};
</script>

Заключение

И вот наконец мы все сделали! Три новых компонента и еще два места, где мы используем Flickr API. Спасибо за чтение и до встречи!

Оригинал: Simple Photo App with Vue.js, Axios, and Flickr API — Part 3


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *