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 publish from "../../core/decorators/publish";
import $clamp from "clamp-js/clamp.js";
import rizzo from "../../rizzo";

class ThingsToDo extends Component {
  get title() {
    return `Top experiences in ${window.lp.place.name}`;
  }
  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();
    });
  }
  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`
    });
  }
  @publish("ttd.removed")
  nukeIt() {
    $("#ttd").remove();
  }
  // TODO: jc this is... smelly
  cardsFetched(cards) {
    if (!cards.length) {
      return this.nukeIt();
    }
    this.$el.prepend($(`<h2 class='ttd__heading'>${this.title}</h2>`));
    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() {
    let $left = $("<div />", { "class": "has-more--left is-invisible"});
    
    // Lazy? Maybe... awesome? YES
    $left.html(`
    <button class="ttd__less js-ttd-less">
      <i class="ttd__more__icon icon-chevron-left" aria-hidden="true"></i>
    </button>
    `);
    this.$el.find(".js-ttd-list").before($left);

    let $right = $("<div />", { "class": "has-more--right"});
    $right.html(`
    <button class="ttd__more js-ttd-more">
      <i class="ttd__more__icon icon-chevron-right" aria-hidden="true"></i>
    </button>
    `);
    this.$el.find(".js-ttd-list").after($right);
  }
  /**
   * 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");
        }));
    });

    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: 300 })
      .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
   */
  @publish("ttd.loadmore");
  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();

    return {
      "direction": "forward"
    };
  }
  @publish("ttd.loadmore");
  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);

    return {
      "direction": "reverse"
    };
  }
  showMore() {
    this.$el.find(".has-more--right").removeClass("is-invisible");
    this.$el.find(".js-ttd-more").removeClass("is-invisible");
  }
  showPrevious() {
    this.$el.find(".has-more--left").removeClass("is-invisible");
    this.$el.find(".js-ttd-less").removeClass("is-invisible");
  }
  showMoreAndPrevious() {
    this.showMore();
    this.showPrevious();
  }
  hideShowMore() {
    this.$el.find(".js-ttd-more").addClass("is-invisible");
    this.$el.find(".has-more--right").addClass("is-invisible");
  }
  hideShowPrevious() {
    this.$el.find(".js-ttd-less").addClass("is-invisible");
    this.$el.find(".has-more--left").addClass("is-invisible");
  }

  /**
   * 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();
      };

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

    return promise;
  }

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

export default ThingsToDo;