Home Reference Source Test Repository

src/components/things_to_do/things_to_do.js

import Component from "../../core/component";
import waitForTransition from "../../core/utils/waitForTransition";
import track from "../../core/decorators/track";
import $clamp from "clamp-js/clamp.js";
import rizzo from "../../rizzo";
import publish from "../../core/decorators/publish";

/**
 * Show a list of Top Experiences
 */
class ThingsToDo extends Component {
  initialize() {
    this.currentIndex = (this.getCurrentIndex()) || 0;

    this.options = {
      numOfCards: 4
    };

    this.events = {
      "click .js-ttd-more": "loadMore",
      "click .js-ttd-less": "loadPrevious",
      "swiperight": "loadPrevious",
      "swipeleft": "loadMore"
    };

    this.fetchCards().done(this.cardsFetched.bind(this)).fail((jqXHR) => {
      rizzo.logger.error({ error: jqXHR.responseText });
      return this.nukeIt();
    });

    this.navigation = require("./things_to_do_navigation.hbs");
  }
  getCurrentIndex() {
    let obj = window.localStorage && JSON.parse(window.localStorage.getItem("ttd.currentIndex"));
    if (!obj || obj.slug !== window.lp.place.slug) {
      return;
    }

    return obj.index;
  }
  fetchCards() {
    return $.ajax({
      url: `/api/${window.lp.place.slug}/experiences.json`
    });
  }
  @publish("experiences.removed")
  nukeIt() {
    $("#experiences").remove();
  }
  // TODO: jc this is... smelly
  cardsFetched(cards) {
    if (!cards.length) {
      return this.nukeIt();
    }
    this.cards = cards;

    if (cards.length > 4) {
      this.addNavigationButtons();
    }
    if (this.currentIndex >= this.options.numOfCards)  {
      this.showPrevious();
    }
    if (this.currentIndex + 4 >= this.cards.length) {
      this.hideShowMore();
    }

    this.template = require("./thing_to_do_card.hbs");
    this.render(this.nextCards());

    this.clampImageCardTitle();
  }
  addNavigationButtons() {
    this.$el.find(".js-ttd-navigation").html(this.navigation());
  }
  /**
   * Get the next 4 cards to render
   * @return {Array} An array of rendered templates
   */
  nextCards() {
    if (this.currentIndex >= this.cards.length) {
      this.currentIndex = 0;
    } else if (this.currentIndex < 0) {
      this.currentIndex = this.cards.length - ((this.cards.length % this.options.numOfCards) || this.options.numOfCards);
    }

    return this.cards.slice(this.currentIndex, this.currentIndex + this.options.numOfCards)
      .map((card, i) => {
        Object.assign(card.card, {
          card_num: i + this.currentIndex + 1,
          order: i
        });
        return this.template(card);
      });
  }

  render(cards) {
    this.$el.find(".js-ttd-list").html(cards.join(""));

    this.loadImages(this.$el.find(".js-image-card-image"));
  }

  loadImages(images) {
    let imagePromises = [];

    images.each((index, element) => {
      let $el = $(element),
          imageUrl = $el.data("image-url"),
          backupUrl = $el.data("backupimage-url");

      imagePromises.push(this.lazyLoadImage(imageUrl)
        .then(undefined, () => {
          return this.lazyLoadImage(backupUrl);
        })
        .then((url) => {
          $el.css({
              "background-image": "url(" + url + ")"
            })
            .addClass("is-visible");
        }, (url) => {
          rizzo.logger.error(`Could not load image: ${url}`);
        }));
    });

    return Promise.all(imagePromises);
  }
  makeNextList() {
    let cards = this.nextCards();
    if (window.localStorage) {
      window.localStorage.setItem("ttd.currentIndex", JSON.stringify({ index: this.currentIndex, slug: window.lp.place.slug }));
    }

    // Create a new list and place it on top of existing list
    return $("<ul />", {
        "class": "ttd__list js-ttd-list"
      })
      .append(cards);
  }
  animate(reverse=false) {
    let $list = this.$el.find(".js-ttd-list"),
        ttdComponentWidth = this.$el.width();

    let $nextList = this.makeNextList();

    $nextList.css({
      "margin-top": `-${$list.outerHeight(true)}px`,
      "transform": `translate3d(${reverse ? "-" : ""}${ttdComponentWidth}px, 0, 0)`
    });
    this.loadImages($nextList.find(".js-image-card-image"));

    this.animating = true;

    $list.after($nextList)
      .css("transform", `translate3d(${reverse ? "" : "-"}${ttdComponentWidth}px, 0, 0)`);

    setTimeout(() => {
      $nextList
        .css("transform", "translate3d(0, 0, 0)");
    }, 30);

    if (!reverse && this.currentIndex + 4 >= this.cards.length) {
      this.hideShowMore();
    } else if (reverse && this.currentIndex - 4 < 0) {
      this.hideShowPrevious();
    }

    return waitForTransition($nextList, { fallbackTime: 600 })
      .then(() => {
        $list.remove();
        $nextList.css("margin-top", 0);
        this.animating = false;
      });
  }
  /**
   * Load more top things to do. Callback from click on load more button.
   * @param  {jQuery.Event} e The DOM event
   */
  @track("Top Experiences More")
  loadMore(e) {
    e.preventDefault();
    if (this.animating || this.currentIndex + 4 >= this.cards.length) {
      return;
    }
    // Grab the next 4 images
    this.showMoreAndPrevious();
    this.currentIndex += this.options.numOfCards;

    // Forward
    this.animate();
  }
  @track("Top Experiences Previous")
  loadPrevious(e) {
    e.preventDefault();
    if (this.animating || this.currentIndex - 4 < 0) {
      return;
    }
    // Grab the next 4 images
    this.showMoreAndPrevious();
    this.currentIndex -= this.options.numOfCards;

    // Reverse
    this.animate(true);
  }
  showMore() {
    this.$el.find(".js-ttd-more").prop("disabled", false);
  }
  showPrevious() {
    this.$el.find(".js-ttd-less").prop("disabled", false);
  }
  showMoreAndPrevious() {
    this.showMore();
    this.showPrevious();
  }
  hideShowMore() {
    this.$el.find(".js-ttd-more").prop("disabled", true);
  }
  hideShowPrevious() {
    this.$el.find(".js-ttd-less").prop("disabled", true);
  }

  /**
   * Lazy load an image
   * @param  {String} url Image url to lazy load
   * @return {Promise} A promise that resolves when the image has loaded
   */
  lazyLoadImage(url) {
    let self = this,
        image = new Image();

    this.imagePromises = this.imagePromises || {};

    if (this.imagePromises[url]) {
      return this.imagePromises[url];
    }

    let promise = new Promise((resolve, reject) => {
      image.src = url;
      image.onload = function() {
        // Only cache the promise when it's successfully loading an image
        self.imagePromises[url] = promise;
        resolve(url);
      };
      image.onerror = function() {
        reject(url);
      };

      if (!url) {
        reject(url);
      }
    });

    return promise;
  }

  /**
   * Clamp a card title
   * @return null
   */
  clampImageCardTitle() {
    $.each($(".js-image-card-title"), function() {
      $clamp($(this).get(0), { clamp: 2 });
    });
  }
}

export default ThingsToDo;