<repo_name>Rubikal/ent-me<file_sep>/db/migrate/20170820145803_create_products.rb
class CreateProducts < ActiveRecord::Migration[5.1]
  def change
    create_table :products do |t|
      t.string    :title
      t.text      :description
      t.string    :image
      t.string    :price
      t.string    :studio
      t.string    :label
      t.integer   :number_of_discs
      t.integer   :duration
      t.integer   :category
      t.integer   :format
      t.datetime  :released_at

      t.timestamps
    end
  end
end
<file_sep>/app/controllers/admin/orders_controller.rb
class Admin::OrdersController < Admin::AdminController

  def index
    @orders = Order.page(params[:page])
  end

  def fulfill
    @order = Order.find(params[:id])
    @order.fulfill
    redirect_to admin_orders_path
  end

  def cancel
    @order = Order.find(params[:id])
    @order.cancel
    redirect_to admin_orders_path
  end

  def show
    @order = Order.find(params[:id])
  end
end
<file_sep>/app/assets/javascripts/review.js
(function(){
  $(document).ready(function() {
    // rating stars
    $("[data-rating]").each(function(){
      var rating = parseFloat($(this).data('rating'));
      $(this).rateYo({
        fullStar: 1,
        rating: rating,
        starWidth: '13px',
        readOnly: $(this).attr('readonly'),
       onChange: function (rating, rateYoInstance) {
          $(this).next().val(rating);
        }
      });
    });

    // validations
    $("#new_review").on('submit', function(){
      if($("[name='review[rating]']").val() === ""){
        alert("Rating required");
        return false;
      }
      return true;
    });

  });
})(jQuery);

<file_sep>/config/routes.rb
Rails.application.routes.draw do


  namespace :api do
    resources :products do
      collection do
        get :search
      end
    end
  end

  resources :news, only: :index

  resources :orders, only: :create do
    collection do
      get :receipt
    end
  end

  resources :checkout, only: [:index]

  resources :carts, only: [:index] do
    collection do
      get :info
      post :add
      post :remove
    end
  end

  match '/about', to: 'home#about', via: :get, as: :about

  devise_for :users, path_names: { sign_in: 'login', sign_out: 'logout', sign_up: 'register' }, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }

  root to: "products#home"

  get '/admin', to: 'admin/products#index'

  resources :products, only: [:index, :show] do
    collection do
      get 'autocomplete'
      get 'home'
      get 'search'
      get ':type', action: :index, constraints: {type: /#{Product::TYPES.map(&:pluralize).join('|')}/}, as: :type
    end
    resources :reviews, only: :create
  end
  resources :products, path: '/product', only: [:show]

  namespace :admin do
    resources :products do
      resources :reviews
    end
    resources :orders do
      member do
        post :fulfill
        post :cancel
      end
    end
    resources :bundles
    resources :news
  end
end
<file_sep>/app/models/bundle.rb
class Bundle < ApplicationRecord
  has_many :bundle_products, autosave: true, dependent: :destroy
  has_many :products, through: :bundle_products, autosave: true

  validates :discount_percentage, presence: true
  validates :discount_percentage, inclusion: { :in => 1..100 }
  validates :products, presence: true
  validates :products, :length => { :minimum => 2, message: "count has to be at least 2" }


  def title
    self.products.first.title rescue nil
  end

  def image
    self.products.first.image rescue nil
  end

  def as_json(options = nil)
    {
      id: self.id,
      title: self.title,
      products: self.products.map(&:as_json),
      discount_percentage: self.discount_percentage
    }
  end
end
<file_sep>/spec/factories/payment_informations.rb
FactoryGirl.define do
  factory :payment_information do
    first_name "MyString"
    last_name "MyString"
    account_number "MyString"
    ccv "MyString"
    expiration_date_month ""
    expiration_date_year 1
  end
end
<file_sep>/app/models/bundle_product.rb
class BundleProduct < ApplicationRecord
  belongs_to :product
  belongs_to :bundle
end
<file_sep>/lib/utilities/cart.rb
class Cart
  def initialize cart
    @cart = cart || {}
  end

  def add_product product, count, set
    count = count.present? ? count.to_i : 1

    if has_item? product
      cart[product] = set ? count : (item_count(product) + count)
    else
      cart[product] = count
    end
  end

  def remove_product product
    if has_item? product
      cart.delete(product)
    end
  end

  def data
    {items: items, count: count, total: total}
  end

  def info
    cart
  end

  def empty?
    cart.keys.length == 0
  end

  def total
    return 0 if empty?

    products.map do |product|
      product.price * cart[product.id.to_s]
    end.sum.round(2)
  end

  private

  def count
    cart.keys.count
  end

  def products
    @products ||= Product.where(id: cart.keys)
  end

  def items
    return [] if empty?

    products.map do |product|
      product_info(product)
    end
  end

  def has_item? product
    cart.keys.include? product
  end

  def item_count product
    cart.fetch(product.to_s, 0)
  end

  def product_info product
    {
      id: product.id,
      title: product.title,
      price: product.price,
      image: product.image_url,
      count: item_count(product.id)
    }
  end

  attr_reader :cart
end

# Cart should looks like {product_id: count, product_id_2: count, ...}
<file_sep>/db/migrate/20170823225848_create_orders.rb
class CreateOrders < ActiveRecord::Migration[5.1]
  def change
    create_table :orders do |t|
      t.float :total_price
      t.float :shipping_cost
      t.string :shipping_method
      t.boolean :ship_to_same_address
      t.belongs_to :user

      t.timestamps
    end
  end
end
<file_sep>/app/models/product.rb
class Product < ApplicationRecord
  include PgSearch

  mount_uploader :image, ::ImageUploader
  pg_search_scope :search_by_title, against: [:title],
    using: {
      tsearch: {
        prefix: true
      }
    }

  TYPES = %w(music film game)
  # Relations
  has_many :bundle_products
  has_many :bundles, through: :bundle_products
  has_many :reviews do
    def average_rating
      average(:rating).to_f
    end
  end

  # Validations
  validates :price, presence: true

  # Scopes
  scope :featured, ->{ where(featured: true) }
  scope :search, ->(keyward){ where("title ilike ?", "%#{keyward}%") }
  scope :by_category, -> (category) { where(category: category) }

  def image_url
    image.url if image
  end

  def as_json(options = nil)
    {
      id: self.id,
      type: self.type,
      title: self.title,
      price: self.price,
      image_url: self.image.thumb.url,
      reviews_average: self.reviews.average_rating
    }
  end
end
<file_sep>/spec/factories/bundles.rb
FactoryGirl.define do
  factory :bundle do
    discount_percentage 1.5
  end
end
<file_sep>/app/models/news.rb
class News < ApplicationRecord
  mount_uploader :image, ::ImageUploader

  def image_url
    image.url if image
  end
end
<file_sep>/app/controllers/admin/bundles_controller.rb
class Admin::BundlesController < Admin::AdminController
  def index
    @bundles = Bundle.page(params[:page])
  end

  def new
    @bundle = Bundle.new
  end

  def show
    @bundle = Bundle.find(params[:id])
  end

  def create
    @bundle = Bundle.new(bundle_params)
    if @bundle.save
      redirect_to admin_bundle_path(@bundle)
    else
      flash[:error] = @bundle.errors.full_messages.to_sentence
      redirect_to new_admin_bundle_path
    end
  end

  def update
    @bundle = Bundle.find(params[:id])
    if @bundle.update_attributes(bundle_params)
      redirect_to admin_bundle_path(@bundle)
    else
      flash[:error] = @bundle.errors.full_messages.to_sentence
      redirect_to admin_bundle_path(@bundle)
    end
  end

  def destroy
    @bundle = Bundle.find(params[:id])
    @bundle.destroy
    redirect_to admin_bundles_path
  end

  protected
  def bundle_params
    params.require(:bundle).permit!
  end
end
<file_sep>/spec/factories/orders.rb
FactoryGirl.define do
  factory :order do
    number_of_items 1
    price 1.5
  end
end
<file_sep>/app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  layout 'mailer'
  default from: '<EMAIL>'

  def receipt_summary(order)
    @user = order.user
    @order = order
    mail(to: @user.email, subject: 'Your order summary from ent-me.com')
  end
end
<file_sep>/README.md
# EntMe


Technologies:

* Ruby version 2.3.1

* Rails version 5.13
  - pg_search for autocomplete
  - devise for authentication 
  - kaminari for pagination 
  - carrierwave for photo upload

* React

* Bootstrap

* jQuery autocomplete


ERD:

![ERD](https://raw.githubusercontent.com/instacular/instacular.github.io/master/img/entme.jpg)


Run project:

* just run the seed and you should be good to go, 2 users created
  - User <EMAIL> / 12345678
  - Admin <EMAIL> / 12345678  Admin should login from user login screen and should be able to find admin tab in header
  
  
All following are supported:

* Ability to view featured items that are marked by the administrator
* View list all the items in each entertainment category
* Ability to search for items on the platform using autocomplete feature
* View the details of each item and add them to the cart
* Ability to checkout his cart and receive and email with the invoice, noting that only
registered users can complete the checkout process.
* Also, Users can register a new account using Facebook to ease the login process to
his/her account.
* Submit a review on any entertainment item with a rating
* Share on Facebook a particular movie with the review submitted

Admin should be able to:

* The administrator should have the ability of creating a bundled item which is a set of
other products on the platform with a discounted price.
* Listing and views of the all the orders to manage them online
* Upload various media types for the entertainment items such as cover images,
trailers, etc…
* Control the reviews submitted on the elements by deleting them if necessary
* Ability to mark orders as fulfilled and label them for future references

Copyright (c) 2017, <EMAIL>
<file_sep>/app/models/game.rb
class Game < Product
  enum category: [:"Action & Adventure", :"Real-time 3D adventures", :Simulation, :Strategy, :Sports, :Art, :Casual, :Educational, :Party, :Programming, :Logic, :Card]
end
<file_sep>/app/models/film.rb
class Film < Product
  enum category: [:Television, :"Children's & Family", :Drama, :"Action & Adventure", :"World Cinema",
                  :Horror, :"Science Fiction", :Comedy, :Documentary, :Thriller, :Western, :"Animated Feature",
                  :Anime, :"Modern Classic", :Musicals, :Sport]

  enum format: [:DVD, :"DVD Box Set", :"Blu-ray"]
end
<file_sep>/app/controllers/admin/reviews_controller.rb
class Admin::ReviewsController < Admin::AdminController

  def index
    @product = Product.find(params[:product_id])
    @reviews = @product.reviews
  end

  def destroy
    @review = Review.find(params[:id])
    @review.destroy
    redirect_to admin_product_reviews_path(product_id: params[:product_id])
  end
end
<file_sep>/app/assets/javascripts/cart.js
(function(){
  $(document).ready(function() {
    // Add item to card
    $('.btn-add-cart').on('click', function() {
      var productID = $(this).data("productid"),
          self = this,
          count = null;

      $(this).prepend('<i class="fa fa-refresh fa-spin"/>');

      if($(this).hasClass('js-with-count')){
        count = $(".quantity").val();
      }
      $.ajax({
        type: "post",
        url: "/carts/add",
        data: {id: productID, count: count},
        success: function(data){
          $(self).find('.fa-refresh').remove();
          renderCart(data);
        },
        failure: function(data){
          $(self).find('.fa-refresh').remove();
          alert("Fail to add item to basket!")
        }
      });
    });

    // Remove item from card
    $('body').on('click', '.remove-item', function(e) {
      e.preventDefault()
      e.stopPropagation()

      var productID = $(this).data("productid");
      $(this).find('.fa').removeClass('fa-times').addClass('fa-refresh fa-spin');
      $(".js-cart-dropdown").addClass("open");
      $.ajax({
        type: "post",
        url: "/carts/remove",
        data: {id: productID},
        success: function(data){
          renderCart(data);
        },
        failure: function(data){
          alert("Fail to add item to basket!")
        }
      });
    });

    // Load cart data
    loadCart()
  });
})(jQuery);

function loadCart(){
  $.ajax({
    type: "get",
    url: "/carts/info",
    success: function(data){
      renderCart(data);
    },
    failure: function(data){
      alert("Fail to load basket data!")
    }
  });
}

function updateCartItem(item, count){
  var productID = $(item).data("productid");

  $.ajax({
    type: "post",
    url: "/carts/add",
    data: {id: productID, count: count, set: true},
    success: function(data){
      renderCart(data);
    },
    failure: function(data){
      alert("Fail to add item to basket!")
    }
  });
}

function renderCart(data){
  renderMiniBascket(data);
  renderBascket(data);
}

function renderMiniBascket(data){
  $(".js-item-count").text([data.count,  " Items"].join(''));
  $(".js-total-price").text(["$", data.total ].join(''));
  $(".js-subtotal").text(["Subtotal: $", data.total ].join(''));

  $(".cart-items").html("");
  if(data.items.length > 0){
    $(".mini-cart-totals").show();
    $.each(data.items, function(index, item){
      item = '<li class="row cart-item">' +
               '<div class="product-thumb col-xs-4">' +
                  '<img class="img-responsive" src='+item.image+' >' +
               '</div>' +
               '<div class="product-info col-xs-8">' +
                  '<h6 class="product-title">'+item.title+'</h6>' +
                  '<div class="price">' +
                     item.count + ' x <span>$' + item.price +'</span>' +
                  '</div>' +
                  '<a class="remove-item" data-productid='+item.id+' href="javascript:;"><i class="fa fa-times"></i></a>' +
               '</div>' +
            '</li>';
      $(".cart-items").append(item);
    });
  }else{
    $(".mini-cart-totals").hide();
    $(".cart-items").append('<h6>No items in cart.</h6>');
  }
}

function renderBascket(data){
  if($(".full-cart-items").length === 0){
    return;
  }

  $(".js-badge").text(["$", data.total ].join(''));
  $(".full-cart-items").html("");
  if(data.items.length > 0){
    $.each(data.items, function(index, item){
      item = '<div class="cart-item row">' +
              '<div class="col-xs-12 col-sm-2">' +
                '<div class="product-thumb">' +
                  '<img class="img-responsive" src='+item.image+'>' +
                '</div>' +
              '</div>' +
              '<div class="col-xs-12 col-sm-4 col-lg-5 cart-details">' +
                '<div class="h4 product-title">' +
                  '<a href="javascript:;">'+item.title+'</a>' +
                  '<p><small class="unit-price"> $'+item.price+'</small></p>' +
                '</div>' +
              '</div>' +
              '<div class="col-xs-12 col-sm-3 col-lg-2 cart-details ">' +
                '<div class="quantity">' +
                  '<div class="quantity-selector">' +
                    '<button class="quantity-btn minus btn btn-lg btn-default js-cart" data-productid='+item.id+'><i class="fa fa-minus"></i></button>' +
                    '<input type="text" class="form-control quantity" value='+item.count+'>' +
                    '<button class="quantity-btn plus btn btn-lg btn-default js-cart" data-productid='+item.id+'><i class="fa fa-plus"></i></button>' +
                  '</div>' +
                '</div>' +
              '</div>' +
              '<div class="col-xs-12 col-sm-3 col-lg-3 cart-details">' +
                '<div class="h4 total-price"> $'+item.price+'</div>' +
                '<a class="remove-item" href="javascript:;" data-productid='+item.id+' ><i class="fa fa-times"></i></a>' +
              '</div>' +
            '</div>'
      $(".full-cart-items").append(item);
    });
  }else{
    $(".full-cart-items").append('<h6>No items in cart.</h6>');
  }
}
<file_sep>/app/assets/javascripts/components/bundle_form.jsx
class BundleForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      searchProducts: [],
      bundleProducts: props.bundle.products || []
    }
  }

  componentDidMount(){
    Ladda.bind( '.ladda-button' );
  }

  searchProducts(e){
    e.preventDefault();
    e.stopPropagation();
    var self = this;
    $.get("/api/products/search", {q: this.searchInput.value}, function(products) {
      Ladda.stopAll();
      self.setState({
        searchProducts: products
      });
    });
  }

  addProduct(product){
    this.state.bundleProducts.push(product);
    this.setState({bundleProducts: this.state.bundleProducts});
  }

  removeProduct(product){
    _.remove(this.state.bundleProducts, function(bundleProduct){
      return bundleProduct.id == product.id;
    });
    this.setState({bundleProducts: this.state.bundleProducts});
  }

  componentDidUpdate(prevProps, prevState){
    $("[data-rating]").each(function(){
      var rating = parseFloat($(this).data('rating'));
      if (rating > 0) {
        $(this).rateYo({
          rating: rating,
          starWidth: '13px',
          readOnly: $(this).attr('readonly')
        });
      }
    });
  }

  render() {
    var self = this;
    return (
      <div>
        <div className="page-header">
          <h4><i className="fa fa-cubes" aria-hidden="true"></i> Add Products</h4>
        </div>
        <form id="search-prodduct-form" onSubmit={this.searchProducts.bind(this)}>
          <div className="grid-form">
            <div className="row">
              <div className="col-sm-12 field-cell">
                <label className="control-label float"><i className="fa fa-search" aria-hidden="true" /> Search products</label>
                <input type="text" name="query" ref={(i) => { this.searchInput = i; }} placeholder="Product title" autoComplete="off" />
                <button id="search-product" type="submit" className="btn btn-default overlay-button ladda-button" data-style="zoom-out" data-spinner-color="#777"><span className="fa fa-search"></span> Search</button>
              </div>
            </div>
          </div>
        </form>
        <div>
          {
            this.state.searchProducts.map((product) => {
              var added = _.some(this.state.bundleProducts, function(bundleProduct){
                return bundleProduct.id == product.id;
              });
              var addButton;
              if (added) {
                addButton = <button type="button" className="btn btn-sm btn-success" disabled><i className="fa fa-check" aria-hidden="true"></i> Added</button>
              }else{
                addButton = <button type="button" className="btn btn-sm btn-primary" onClick={self.addProduct.bind(self, product)}><i className="fa fa-plus" aria-hidden="true"></i> Add</button>
              }

              return(
                <div className="ui-menu-item" key={product.id}>
                  <span className="img">
                    <img src={ product.image_url } />
                  </span>
                  <span className="title"> { product.title }
                    {addButton}
                  </span>
                  <span data-rating={ product.reviews_average } readOnly></span>
                  <span className="price"> ${ product.price } </span>
                </div>
              )
            })
          }
        </div>
        <form id="new-bundle-form" className="simple_form form-horizontal" noValidate="novalidate" encType="multipart/form-data" action={self.props.bundle.id ? ("/admin/bundles/" + self.props.bundle.id) : "/admin/bundles"} acceptCharset="UTF-8" method="post">
          {self.props.bundle.id ? <input type="hidden" name="_method" value="patch" /> : null }

          <div className="page-header">
            <h4><i className="fa fa-tags" aria-hidden="true"></i> Bundle Products</h4>
          </div>
          <div>
          {
            this.state.bundleProducts.map((product) => {
              return(
                <div className="ui-menu-item" key={product.id}>
                  <span className="img">
                    <img src={ product.image_url } />
                  </span>
                  <span className="title"> { product.title }
                    <button type="button" className="btn btn-sm btn-danger" onClick={self.removeProduct.bind(self, product)}><i className="fa fa-remove" aria-hidden="true"></i> Remove</button>
                  </span>
                  <span data-rating={ product.reviews_average } readOnly></span>
                  <span className="price"> ${ product.price } </span>
                  <input type="hidden" name="bundle[product_ids][]" value={product.id} />
                </div>
              )
            })
          }
          </div>
          <br />
          <div className="page-header">
            <h4><i className="fa fa-percent" aria-hidden="true"></i> Discount</h4>
          </div>
          <div className="grid-form">
            <div className="row">
              <div className="col-sm-12 field-cell">
                <div className="form-group float required bundle_discount_percentage">
                  <div>
                    <input className="form-control input-sm numeric float required" placeholder="Discount percentage" type="number" step="any" name="bundle[discount_percentage]" id="bundle_discount_percentage" defaultValue={self.props.bundle.discount_percentage} />
                  </div>
                  <label className="control-label float required" htmlFor="bundle_discount_percentage">
                    <abbr title="required">*</abbr> Discount percentage (%)
                  </label>
                </div>
              </div>
            </div>
          </div>
          <div className="section">
            <button id="bundle-form-submit" className="btn btn-default ladda-button pull-right" data-style="zoom-out" data-spinner-color="#777">
              <span className="ladda-label">
                <i className="fa fa-check">&nbsp;</i>
                Save
              </span>
            </button>
            <div className="clearfix" />
          </div>
        </form>
      </div>
    );
  }
}<file_sep>/app/controllers/api/products_controller.rb
class Api::ProductsController < ApplicationController

  def search
    query = params[:q]
    render json: Product.search_by_title(query).limit(10)
  end
end
<file_sep>/db/seeds.rb
if User.count === 0
  User.new.tap do |user|
    user.name = Faker::Name.name
    user.email = "<EMAIL>"
    user.password = "<PASSWORD>"
    user.skip_confirmation!
    user.save!
  end

  User.new.tap do |user|
    user.name = Faker::Name.name
    user.email = "<EMAIL>"
    user.password = "<PASSWORD>"
    user.skip_confirmation!
    user.admin = true
    user.save!
  end
end

if Product.count == 0
  user = User.where(admin: false).first
  [Film, Game, Music].each do |klass|
    klass.categories.keys.each do |category|
      10.times do |no|
        product = klass.new.tap do |p|
          p.title = Faker::Commerce.product_name
          p.description = Faker::Lorem.paragraph(2)
          p.price = Faker::Commerce.price
          p.category = category
          p.featured = no < 3
          p.image = File.open("app/assets/images/#{klass.to_s.downcase}#{no%3 + 1}.jpg")
          p.save!
          3.times.each do
            p.reviews.create!(user: user, rating: (1..5).to_a.shuffle.first, message: Faker::Lorem.paragraph)
          end
        end
        product
      end
    end
  end
end

if News.count == 0
  20.times do |no|
    News.new.tap do |news|
      news.title = Faker::Commerce.product_name
      news.description = Faker::Lorem.paragraph(2)
      news.image = File.open("app/assets/images/news#{no%3 + 1}.jpg")
      news.save!
    end
  end
end
<file_sep>/app/assets/javascripts/components/checkout.jsx
class Checkout extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      step: 1,
      showBillingInfo: false,
      firstStepValid: true,
      secondStepValid: true,
      thirdStepValid: true,
      billingInfo: {
        first_name: "",
        last_name: "",
        email: "",
        phone: "",
        address: "",
        city: "",
        postal_code: "",
        country: "Algeria"
      },
      shippingInfo:{
        first_name: "",
        last_name: "",
        phone: "",
        address: "",
        city: "",
        postal_code: "",
        country: "Algeria"
      },
      paymentInfo:{
        first_name: "",
        last_name: "",
        accunt_number: "",
        cvv: "",
        expiration_date_month: "",
        expiration_date_year: ""
      }
    };
  }

  componentDidMount(){
    $('[data-toggle="tooltip"]').tooltip();
    $(".remove-item").remove(); // Don't allow bascket edits anymore
  }

  activateStep(step, e){
    if(e){
      e.preventDefault();
      e.stopPropagation();
    }

    this.setState({step: step});
  }

  renderSteps(){
    return(
      <div>
        {this.renderFirstStep()}
        {this.renderSecondStep()}
        {this.renderThirdStep()}
      </div>
    );
  }

  handleInputChange(obj, e){
    var name = e.target.name,
        value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;

    this.state[obj][name] = value;
    this.setState(this.state);
  }

  validateFirstStep(e){
    e.preventDefault();
    e.stopPropagation();
    var billingInfo = this.state.billingInfo,
        shippingInfo = this.state.shippingInfo,
        valid = false;
    if(billingInfo.first_name.trim() != "" && billingInfo.last_name.trim() != "" &&
       billingInfo.email.trim() != "" && billingInfo.phone.trim() != "" &&
       billingInfo.address.trim() != "" && billingInfo.city.trim() != "" &&
       billingInfo.postal_code.trim() != "" && billingInfo.country.trim() != ""){
      if(this.state.showBillingInfo){
        if(shippingInfo.first_name.trim() != "" && shippingInfo.last_name.trim() != "" &&
           shippingInfo.phone.trim() != "" && shippingInfo.address.trim() != "" &&
           shippingInfo.city.trim() != "" && shippingInfo.postal_code.trim() != "" &&
           shippingInfo.country.trim() != ""){

           valid = true;
        }
      }else{
        valid = true;
      }
    }
    if(valid){
      this.state.firstStepValid = true;
      this.activateStep(2);
    }else{
      alert("All fields are required!");
      this.state.firstStepValid = false;
      this.setState(this.state);
    }
  }

  validateSecondStep(e){
    e.preventDefault();
    e.stopPropagation();

    this.activateStep(3);
  }

  validateThirdStep(e){
    e.preventDefault();
    e.stopPropagation();

    var paymentInfo = this.state.paymentInfo,
        valid = false;
    if(paymentInfo.first_name.trim() != "" && paymentInfo.last_name.trim() != "" &&
       paymentInfo.accunt_number.trim() != "" && paymentInfo.cvv.trim() != "" &&
       paymentInfo.expiration_date_month.trim() != "" && paymentInfo.expiration_date_year.trim() != ""){
        valid = true;
    }
    if(valid){
      this.state.thirdStepValid = true;
      this.saveForm();
    }else{
      alert("All fields are required!");
      this.state.thirdStepValid = false;
      this.setState(this.state);
    }
  }

  saveForm(){
    order = { billing_information: this.state.billingInfo,
              shipping_information: this.state.shippingInfo,
              payment_information: this.state.paymentInfo,
              shipping_cost: 5,
              shipping_method: "Sample shipping method",
              ship_to_same_address: !this.state.showBillingInfo }

    $.ajax({
      type: "post",
      url: "/orders",
      data: {order: order},
      success: function(data){
        window.location.href = "/orders/receipt";
      },
      failure: function(data){
        alert("Fail to place order!")
      }
    });
  }

  toggleShowBillingInfo(e){
    e.preventDefault();
    e.stopPropagation();
    this.setState({showBillingInfo: !this.state.showBillingInfo});
  }

  renderFirstStep(){
    var stepContainerClass =  this.state.step === 1 ? "col-sm-12 col-md-9" : "col-sm-12 col-md-9 hide",
        billingInfo = this.state.billingInfo,
        shippingInfo = this.state.shippingInfo,
        shippingContainerClass = this.state.showBillingInfo ? "row" : "row hide";

    return(
      <div className={stepContainerClass}>
        <div id="checkout-page">
          <form className="custom">
            <h2>Billing Information</h2>
            <div id="billing-info" className="row">
              <div className={!this.state.firstStepValid && billingInfo.first_name.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" } >
                <input data-mirror="" type="text" className="form-control" name="first_name" value={billingInfo.first_name}  onChange={this.handleInputChange.bind(this, "billingInfo")}  placeholder="<NAME>"/>
                <label htmlFor="billing_firstName">First Name</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.last_name.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="last_name"  value={billingInfo.last_name}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="<NAME>"/>
                <label htmlFor="billing_lastName">Last Name</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.email.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input type="email" className="form-control" name="email" value={billingInfo.email}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="E-mail Address"/>
                <label htmlFor="billing_email">E-mail Address</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.phone.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="phone" value={billingInfo.phone}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="Phone Number"/>
                <label htmlFor="billing_phone">Phone Number</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.address.trim() === "" ? "col-sm-12 form-group has-error" : "col-sm-12 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="address" value={billingInfo.address}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="Address"/>
                <label htmlFor="billing_address">Address</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.city.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="city" value={billingInfo.city}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="City"/>
                <label htmlFor="billing_city">City</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && billingInfo.postal_code.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="postal_code" value={billingInfo.postal_code}  onChange={this.handleInputChange.bind(this, "billingInfo")} placeholder="Zip Code"/>
                <label htmlFor="billing_postalCode">Zip Code</label>
                <span className="error"></span>
              </div>
              <div className="col-sm-6 form-group">
                <label htmlFor="billing_country">Country</label>
                <select className="chzn-select form-control" name="country" value={billingInfo.country}  onChange={this.handleInputChange.bind(this, "billingInfo")}>
                  <option value="Algeria">Algeria</option>
                  <option value="Brazil">Brazil</option>
                  <option value="Canada">Canada</option>
                  <option value="Cape Verde">Cape Verde</option>
                  <option value="China">China</option>
                  <option value="Czech Republic">Czech Republic</option>
                  <option value="Egypt">Egypt</option>
                  <option value="France">France</option>
                  <option value="United Kingdom">United Kingdom</option>
                  <option value="United States">United States</option>
                  <option value="Zambia">Zambia</option>
                  <option value="Zimbabwe">Zimbabwe</option>
                </select>
                <span className="error"></span>
              </div>
            </div>
            <h2 className="inline-block">Shipping Information</h2>
            <div className="h6">
              <a className="btn btn-default" href="#" onClick={this.toggleShowBillingInfo.bind(this)}>

              {!this.state.showBillingInfo && <i className="fa fa-check"></i> }
              {this.state.showBillingInfo && <i className="fa" style={{visibility: "hidden"}}></i> }

              </a>
              &nbsp; Same as Billing Information
            </div>
            <div id="shipping-info" className={shippingContainerClass}>
              <div className={!this.state.firstStepValid && shippingInfo.first_name.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="first_name" value={shippingInfo.first_name}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="<NAME>"/>
                <label htmlFor="shipping_firstName">First Name</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && shippingInfo.last_name.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="last_name" value={shippingInfo.last_name}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="<NAME>"/>
                <label htmlFor="shipping_lastName">Last Name</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && shippingInfo.phone.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="phone" value={shippingInfo.phone}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="Phone Number"/>
                <label htmlFor="shipping_phone">Phone Number</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && shippingInfo.address.trim() === "" ? "col-sm-12 form-group has-error" : "col-sm-12 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="address"  value={shippingInfo.address}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="Address"/>
                <label htmlFor="shipping_address">Address</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && shippingInfo.city.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="city" value={shippingInfo.city}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="City"/>
                <label htmlFor="shipping_city">City</label>
                <span className="error"></span>
              </div>
              <div className={!this.state.firstStepValid && shippingInfo.postal_code.trim() === "" ? "col-sm-6 form-group has-error" : "col-sm-6 form-group" }>
                <input data-mirror="" type="text" className="form-control" name="postal_code" value={shippingInfo.postal_code}  onChange={this.handleInputChange.bind(this, "shippingInfo")} placeholder="Zip Code"/>
                <label htmlFor="shipping_postalCode">Zip Code</label>
                <span className="error"></span>
              </div>
              <div className="col-sm-6 form-group">
                <label htmlFor="shipping_country">Country</label>
                <select data-mirror="" className="chzn-select form-control" name="country" value={shippingInfo.country}  onChange={this.handleInputChange.bind(this, "shippingInfo")}>
                  <option value="Algeria">Algeria</option>
                  <option value="Brazil">Brazil</option>
                  <option value="Canada">Canada</option>
                  <option value="Cape Verde">Cape Verde</option>
                  <option value="China">China</option>
                  <option value="Czech Republic">Czech Republic</option>
                  <option value="Egypt">Egypt</option>
                  <option value="France">France</option>
                  <option value="United Kingdom">United Kingdom</option>
                  <option value="United States">United States</option>
                  <option value="Zambia">Zambia</option>
                  <option value="Zimbabwe">Zimbabwe</option>
                </select>
                <span className="error"></span>
              </div>
            </div>
            <div className="clearfix">
              <a className="btn btn-primary btn-lg btn-checkout-step pull-right" href="#" onClick={this.validateFirstStep.bind(this)} >Next Step <i className="fa fa-arrow-right"></i></a>
            </div>
          </form>
        </div>
      </div>
    );
  }

  renderSecondStep(){
    var stepContainerClass =  this.state.step === 2 ? "col-sm-12 col-md-9" : "col-sm-12 col-md-9 hide";

    return(
      <div className={stepContainerClass}>
        <div id="checkout-page">
          <div className="custom">
            <h2>Shipping Method</h2>
            <input type="hidden" name="shippingMethod" />
            <table className="table table-bordered" id="shipping-methods">
              <tbody>
                <tr>
                  <td>
                    <div className="h6">
                      <a className="btn btn-default" href="javascript:;">
                      <i className="fa fa-check"></i>
                      </a>
                      &nbsp; Sample shipping method
                    </div>
                  </td>
                  <td className="narrow">
                    <span className="h3"> $5.00
                    </span>
                  </td>
                </tr>
              </tbody>
            </table>
            <div className="clearfix">
              <a className="btn btn-default btn-lg btn-checkout-step pull-left" href="#" onClick={this.activateStep.bind(this, 1)}><i className="fa fa-arrow-left fa-arrow-right" /> Previous Step</a>
              <a className="btn btn-primary btn-lg btn-checkout-step pull-right" href="#" onClick={this.validateSecondStep.bind(this)} >Next Step <i className="fa fa-arrow-right" /></a>
            </div>
          </div>
        </div>
      </div>
    );
  }

  renderThirdStep(){
    var stepContainerClass = this.state.step === 3 ? "col-sm-12 col-md-9" : "col-sm-12 col-md-9 hide",
        paymentInfo = this.state.paymentInfo;
    return(
      <div className={stepContainerClass}>
        <div id="checkout-page">
          <h2>Payment Method</h2>
          <div id="payment_form">
            <h6>This is a test payment method. Never use your real credit card information in this form.</h6>
            <div>
              <div className="form row">
                <div className={!this.state.thirdStepValid && paymentInfo.first_name.trim() === "" ? "col-sm-6 form-group text left has-error" : "col-sm-6 form-group text left" }>
                  <input className="form-control" autoComplete="off" name="first_name" id="FIRSTNAME" type="text" placeholder="<NAME>" value={paymentInfo.first_name}  onChange={this.handleInputChange.bind(this, "paymentInfo")} />
                  <label htmlFor="FIRSTNAME"><NAME> Name</label>
                </div>
                <div className={!this.state.thirdStepValid && paymentInfo.last_name.trim() === "" ? "col-sm-6 form-group text right has-error" : "col-sm-6 form-group text right" }>
                  <input className="form-control" autoComplete="off" name="last_name" id="LASTNAME" type="text" placeholder="<NAME>" value={paymentInfo.last_name}  onChange={this.handleInputChange.bind(this, "paymentInfo")}/>
                  <label htmlFor="LASTNAME"><NAME> Name</label>
                </div>
                <div className={!this.state.thirdStepValid && paymentInfo.accunt_number.trim() === "" ? "col-sm-6 form-group text valid has-error" : "col-sm-6 form-group text valid" }>
                  <input className="form-control credit-card-input" autoComplete="off" name="accunt_number" id="ACCT" type="text" placeholder="Credit Card Number" value={paymentInfo.accunt_number}  onChange={this.handleInputChange.bind(this, "paymentInfo")}/>
                  <label htmlFor="ACCT">visa <i className="fa fa-check-circle" /></label>
                </div>
                <div className={!this.state.thirdStepValid && paymentInfo.cvv.trim() === "" ? "col-sm-6 form-group text helper has-error" : "col-sm-6 form-group text helper" }>
                  <input className="form-control" autoComplete="off" name="cvv" id="CVV2" type="text" placeholder="CVV2" value={paymentInfo.cvv}  onChange={this.handleInputChange.bind(this, "paymentInfo")}  />
                  <label htmlFor="CVV2">CVV2</label>
                  <i className="fa fa-question" data-container="body" data-toggle="tooltip" title="For MasterCard, Visa, and Discover, the CSC is the last three digits in the signature area on the back of your card. For American Express, it's the four digits on the front of the card." />
                </div>
                <div className={!this.state.thirdStepValid && paymentInfo.expiration_date_month.trim() === "" ? "col-sm-6 form-group select left has-error" : "col-sm-6 form-group select left" }>
                  <input className="form-control" type="text" name="expiration_date_month" id="EXPDATE_MONTH" placeholder="Expiration Date - Month" value={paymentInfo.expiration_date_month}  onChange={this.handleInputChange.bind(this, "paymentInfo")}/>
                  <label htmlFor="EXPDATE_MONTH">Expiration Date - Month</label>
                </div>
                <div className={!this.state.thirdStepValid && paymentInfo.expiration_date_month.trim() === "" ? "col-sm-6 form-group text right has-error" : "col-sm-6 form-group text right" }>
                  <input className="form-control" type="text" name="expiration_date_year" id="EXPDATE_YEAR" placeholder="Expiration Date - Year" value={paymentInfo.expiration_date_year}  onChange={this.handleInputChange.bind(this, "paymentInfo")} />
                  <label htmlFor="EXPDATE_YEAR">Expiration Date - Year</label>
                </div>
              </div>
              <div className="clearfix">
                <a className="btn btn-default btn-lg btn-checkout-step pull-left" href="#" onClick={this.activateStep.bind(this, 2)}><i className="fa fa-arrow-left" /> Previous Step</a>
                <button className="btn btn-danger solid btn-lg btn-checkout-step pull-right" onClick={this.validateThirdStep.bind(this)} >Pay Now <i className="fa fa-money" /></button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

  renderCartInfo(){
    var total = this.state.step == 3 ? this.props.cartTotal+5 : this.props.cartTotal
    return(
      <div id="cart-totals" className="col-sm-12 col-md-3">
        <div id="cart-summary">
          <h3 className="cart-summary-title">Order Summary</h3>
          <ul className="price-list list-group">
            <li className="list-group-item">Subtotal: <span className="badge">${this.props.cartTotal}</span></li>
            { this.state.step===3 &&
              <li className="list-group-item">Shipping: <span className="badge">$5.00</span></li>
            }
            <li className="list-group-item">Tax: <span className="badge">$0.00</span></li>
            <li className="list-group-item order-total h4 clearfix"><span>Total:</span> <span className="badge">${total}</span></li>
          </ul>
        </div>
      </div>
    );
  }

  render() {
    var widget = this,
        secondStepClass = this.state.step > 1 ? "active" : "",
        thirdStepClass = this.state.step > 2 ? "active" : "" ;
    return(
      <div>
        <div id="banner" className="jumbotron row">
          <h1>Checkout</h1>
        </div>
        <section id="checkout" className="page-section row">
          <div id="checkout-progress">
            <ul className="breadcrumb list-inline checkout-progress">
              <li className="active">1. Billing Address</li>
              <li className={secondStepClass} >2. Shipping Method</li>
              <li className={thirdStepClass} >3. Pay</li>
            </ul>
          </div>
          {widget.renderSteps()}
          {widget.renderCartInfo()}
        </section>
      </div>
    );
  }
}
<file_sep>/db/migrate/20170823231142_create_payment_informations.rb
class CreatePaymentInformations < ActiveRecord::Migration[5.1]
  def change
    create_table :payment_informations do |t|
      t.string :first_name
      t.string :last_name
      t.string :account_number
      t.string :cvv
      t.integer :expiration_date_month
      t.integer :expiration_date_year
      t.belongs_to :order

      t.timestamps
    end
  end
end
<file_sep>/app/helpers/application_helper.rb
module ApplicationHelper
  def controller?(*controller)
    controller.include?(params[:controller])
  end

  def action?(*action)
    action.include?(params[:action])
  end

  def bootstrap_class_for flash_type
    { success: "alert-success", error: "alert-danger", alert: "alert-warning", notice: "alert-info" }[flash_type] || flash_type.to_s
  end

  def flash_messages(opts = {})
    flash.each do |msg_type, message|
      concat(content_tag(:div, message, class: "alert #{bootstrap_class_for(msg_type.to_sym)} fade in") do
              concat content_tag(:button, 'x', class: "close", data: { dismiss: 'alert' })
              if message.is_a?(Array)
                message.each do |m|
                  concat content_tag(:div, m)
                end
              else
                concat message
              end
            end)
    end
    nil
  end

  def tinymce(config=:default, options={})
    javascript_tag { tinymce_javascript(config, options) }
  end

  # Returns the JavaScript code required to initialize TinyMCE.
  def tinymce_javascript(config=:default, options={})
    <<-JAVASCRIPT.strip_heredoc.html_safe
    (function() {
      function initTinyMCE() {
        if (typeof tinyMCE != 'undefined') {
          tinyMCE.init(#{to_javascript(tinymce_configuration(config, options)).gsub(/^/, ' ' * 12).sub(/\A\s+/, "")});
        } else {
          setTimeout(initTinyMCE, 50);
        }
      }
      initTinyMCE();
    })();
    JAVASCRIPT
  end

  # Returns the TinyMCE configuration object.
  # It should be converted to JavaScript (via #to_javascript) for use within JavaScript.
  def tinymce_configuration(config=:default, options={})
    {selector:'.tinymce', menubar: false, min_height: 170, max_height: 220, branding: false}.merge(options)
  end

  def to_javascript(options)
    pairs = options.inject([]) do |result, (k, v)|
      if v.respond_to?(:to_javascript)
        v = v.to_javascript
      elsif v.respond_to?(:to_json)
        v = v.to_json
      end

      result << [k, v].join(": ")
    end

    "{\n  #{pairs.join(",\n  ")}\n}"
  end

  def nav_name_for product
    product.class.to_s.downcase.pluralize
  end

  def review_time review
    review.created_at.strftime("%I:%M %P")
  end

  def review_date review
    review.created_at.strftime("%-d %b %Y")
  end
end
<file_sep>/app/models/music.rb
class Music < Product
    enum category: [:"Rock & Pop", :Classical, :"Rock & Roll", :Blues, :"R&B & Soul", :Metal, :Dance,
                    :"Easy Listening", :Country, :Jazz, :"Rock Pop", :Folk, :"Rap & Hip-Hop", :Reggae,
                    :"Rock&Roll", :Soundtracks, :"Spoken Word"]

    enum format: [:"CD Box Set", :"CD Album", :"Vinyl 12\" Box Set", :"Vinyl 12\" Album", :"CD/DVD Album",
                  :"7\" Vinyl Single Box Set", :"12\" Vinyl/CD Album", :"CD/Blu-ray Album", :"CD/DVD/Blu-ray Album",
                  :"12\" Vinyl Single", :"12\" Vinyl/DVD Album", :CD, :"SACD/DVD Album", :Vinyl]
end
<file_sep>/app/controllers/admin/admin_controller.rb
class Admin::AdminController < ApplicationController
  layout 'admin_application'
  before_action :authenticate_admin!

  protected
  def authenticate_admin!
    redirect_to new_user_session_path unless user_signed_in? && current_user.admin?
  end
end
<file_sep>/spec/factories/bundle_products.rb
FactoryGirl.define do
  factory :bundle_product do
    
  end
end
<file_sep>/db/migrate/20170822165224_change_price_column_type_for_product.rb
class ChangePriceColumnTypeForProduct < ActiveRecord::Migration[5.1]
  def change
    change_column :products, :price, 'float USING price::double precision'
  end
end
<file_sep>/app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  before_action :authenticate_user!

  def create
    data = cart.data

    order = nil

    ActiveRecord::Base.transaction do
      order = current_user.orders.create!(order_params.merge({ total_price: data[:total] }))

      BillingInformation.create!(billing_params.merge(order: order))
      ShippingInformation.create!(shipping_params.merge(order: order)) unless order.ship_to_same_address
      PaymentInformation.create!(payment_params.merge(order: order))

      data[:items].each do |item|
        order.line_items.create!(product_id: item[:id], quantity: item[:count])
      end
    end

    # Send email with receipt to user
    UserMailer.receipt_summary(order).deliver_now if order && order.persisted?

    session[:cart] = {}  # Clear shopping cart

    render json: [], status: 200
  end

  def receipt
    @order = Order.includes(:line_items).last
    redirect_to root_path and return unless @order
  end

  private
  def cart
    @cart ||= Cart.new(session[:cart])
  end

  def order_params
    params.require(:order).permit(:shipping_cost, :shipping_method, :ship_to_same_address)
  end

  def billing_params
    params.require(:order).require(:billing_information).permit(:first_name, :last_name, :email, :phone, :address, :city, :postal_code, :country)
  end

  def shipping_params
    params.require(:order).require(:shipping_information).permit(:first_name, :last_name, :phone, :address, :city, :postal_code, :country)
  end

  def payment_params
    params.require(:order).require(:payment_information).permit(:first_name, :last_name, :account_number, :cvv, :expiration_date_month, :expiration_date_year)
  end
end
<file_sep>/app/controllers/admin/news_controller.rb
class Admin::NewsController < Admin::AdminController

  def index
    @news = News.page(params[:page])
  end

  def new
    @news = News.new
  end

  def show
    @news = News.find(params[:id])
  end

  def create
    @news = News.new(news_params)
    if @news.save
      redirect_to admin_news_index_path
    else
      flash[:error] = @news.errors.full_messages.to_sentence
      redirect_to new_admin_news_path
    end
  end

  def update
    @news = News.find(params[:id])
    if @news.update_attributes(news_params)
      redirect_to admin_news_path(@news)
    else
      flash[:error] = @news.errors.full_messages.to_sentence
      redirect_to admin_news_path(@news)
    end
  end

  def destroy
    @news = News.find(params[:id])
    @news.destroy
    redirect_to admin_bundles_path
  end

  protected
  def news_params
    params.require(:news).permit!
  end
end
<file_sep>/app/controllers/products_controller.rb
class ProductsController < ApplicationController
  before_action :load_class

  def home
    @products = Product.featured.take(3)
    @featured = @products.any?

    unless @featured
      @products = Product.take(3)
    end
  end

  def autocomplete
    products = Product.search_by_title(params[:term]).with_pg_search_highlight
    render json: products.map {|p| { id: p.id, title: p.title, image_url: p.image_url, price: p.price } }, status: 200
  end

  def index
    @products = @klass.page(params[:page]).per(6)
    if @category = params[:category]
      @products = @products.by_category(@category)
    end
    @categories = @klass.categories.keys
    @products_categories_count = @klass.group(:category).count
    @type = params[:type]
  end

  def show
    @product = Product.find params[:id]
    @reviews = @product.reviews.includes(:user)
    @reviews_rating = @reviews.average_rating

    @review = @product.reviews.new if current_user
  end

  def search
    @search = params[:keyward]
    @products = Product.search(@search)
  end

  protected
  def load_class
    @klass = params[:type].singularize.classify.constantize rescue Product
  end
end
<file_sep>/app/controllers/admin/products_controller.rb
class Admin::ProductsController < Admin::AdminController
  before_action :constrain_type
  before_action :require_type, only: [:new, :create]
  before_action :load_class, only: [:index, :new, :create]

  def index
    @products = @klass.page(params[:page])
    if params[:category]
      @products = @products.by_category(params[:category])
    end
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = @klass.new
  end

  def create
    @product = @klass.new product_params
    if @product.save
      redirect_to admin_products_path
    else
      render :new
    end
  end

  def update
    @product = Product.find(params[:id])
    if @product.update_attributes(product_params)
      redirect_to admin_product_path(@product)
    else
      render :show
    end
  end

  protected
  def load_class
    @klass = params[:type].singularize.classify.constantize rescue Product
  end

  def product_params
    case params[:type]
    when 'film'
      film_params
    when 'music'
      music_params
    when 'game'
      game_params
    end
  end

  def film_params
    params.require(:film).permit(:title, :description, :category, :price, :duration, :number_of_discs, :format, :image)
  end

  def music_params
    params.require(:film).permit(:title, :description, :category, :price, :format, :released_at, :image)
  end

  def game_params
    params.require(:film).permit(:title, :description, :category, :price, :released_at, :image)
  end

  def constrain_type
    head(:bad_request) if params[:type].present? && !(params[:type] =~ /^(#{Product::TYPES.join('|')})$/)
  end

  def require_type
    head(:bad_request) if !params[:type].present?
  end
end
<file_sep>/app/controllers/news_controller.rb
class NewsController < ApplicationController
  def index
    @news = News.page(params[:page] || 1).per(6)
  end
end
<file_sep>/app/models/order.rb
class Order < ApplicationRecord
  enum status: [:open, :fulfilled, :cancelled]

  # Relations
  belongs_to :user
  has_many :line_items
  has_one :billing_information
  has_one :shipping_information
  has_one :payment_information

  def total
    self.line_items.map(&:product).map(&:price).sum + self.shipping_cost
  end

  def fulfill
    self.fulfilled!
  end

  def cancel
    self.cancelled!
  end
end
<file_sep>/app/models/shipping_information.rb
class ShippingInformation < ApplicationRecord
  belongs_to :order
end
<file_sep>/app/controllers/checkout_controller.rb
class CheckoutController < ApplicationController
  def index
    @cart_empty = cart.empty?
    @cart_total = cart.total
  end

  private

  def cart
    @cart ||= Cart.new(session[:cart])
  end
end
<file_sep>/app/controllers/carts_controller.rb
class CartsController < ApplicationController
  def index
    @cart = cart.data
  end

  def info
    render json: cart.data, status: 200
  end

  def add
    product = cart.add_product(params[:id], params[:count], params[:set])
    session[:cart] = cart.info

    render json: cart.data, status: 200
  end

  def remove
    cart.remove_product(params[:id])
    session[:cart] = cart.info

    render json: cart.data, status: 200
  end

  private

  def cart
    @cart ||= Cart.new(session[:cart])
  end
end
<file_sep>/spec/factories/reviews.rb
FactoryGirl.define do
  factory :review do
    rating 1
    review "MyText"
  end
end
<file_sep>/app/assets/javascripts/main.js
(function(){
  $(document).ready(function() {
    // Product increment/decrement count
    $(document).on('click', '.quantity-selector .minus', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var $el = $(this).siblings('.quantity'),
        q = parseInt($el.val())-1;
      if (q >= 1) {
        $el.val(q);

        if($(this).hasClass("js-cart")){
          updateCartItem(this, q);
        }
      }
    });

    $(document).on('click', '.quantity-selector .plus', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var $el = $(this).siblings('.quantity'),
        q = parseInt($el.val())+1;
      $el.val(q);

      if($(this).hasClass("js-cart")){
        updateCartItem(this, q);
      }
    });

    var winWidth = $(window).width();
    $(window).resize(function() {
      winWidth = $(window).width();
    });

    // News scroll
    $('.scroll-perfect').perfectScrollbar();
  });
})(jQuery);
