diff --git a/.gitignore b/.gitignore
index 6db3c9a5bc..c43c6b0bf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,9 +7,14 @@
# Ignore bundler config.
/.bundle
+# Ignore the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-journal
+
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
-
+coverage
+.env
.DS_Store
diff --git a/.ruby-gemset b/.ruby-gemset
deleted file mode 100644
index d5c660823f..0000000000
--- a/.ruby-gemset
+++ /dev/null
@@ -1 +0,0 @@
-betsy
diff --git a/.ruby-version b/.ruby-version
deleted file mode 100644
index 2bf1c1ccf3..0000000000
--- a/.ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-2.3.1
diff --git a/Gemfile b/Gemfile
index c69f4ddde9..d89f616a42 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,10 +1,10 @@
source 'https://rubygems.org'
-ruby '2.3.1'
+
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
-gem 'rails', '4.2.6'
-# Use postgresql as the database for Active Record
-# gem 'pg', '~> 0.15'
+gem 'rails', '4.2.7'
+# Use sqlite3 as the database for Active Record
+gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
@@ -22,6 +22,8 @@ gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc
+gem "omniauth"
+gem "omniauth-github"
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
@@ -35,10 +37,21 @@ gem 'sdoc', '~> 0.4.0', group: :doc
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
+ gem 'dotenv-rails'
+end
+
+
+group :test do
+ gem 'minitest-reporters'
+ gem 'simplecov'
end
group :development do
# Access an IRB console on exception pages or by using <%= console %> in views
+
+ gem "better_errors"
+ gem "binding_of_caller"
+ gem "pry-rails"
gem 'web-console', '~> 2.0'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
diff --git a/Gemfile.lock b/Gemfile.lock
index 20975578b8..c9a6ee3c34 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,46 +1,52 @@
GEM
remote: https://rubygems.org/
specs:
- actionmailer (4.2.6)
- actionpack (= 4.2.6)
- actionview (= 4.2.6)
- activejob (= 4.2.6)
+ actionmailer (4.2.7)
+ actionpack (= 4.2.7)
+ actionview (= 4.2.7)
+ activejob (= 4.2.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.6)
- actionview (= 4.2.6)
- activesupport (= 4.2.6)
+ actionpack (4.2.7)
+ actionview (= 4.2.7)
+ activesupport (= 4.2.7)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.6)
- activesupport (= 4.2.6)
+ actionview (4.2.7)
+ activesupport (= 4.2.7)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (4.2.6)
- activesupport (= 4.2.6)
+ activejob (4.2.7)
+ activesupport (= 4.2.7)
globalid (>= 0.3.0)
- activemodel (4.2.6)
- activesupport (= 4.2.6)
+ activemodel (4.2.7)
+ activesupport (= 4.2.7)
builder (~> 3.1)
- activerecord (4.2.6)
- activemodel (= 4.2.6)
- activesupport (= 4.2.6)
+ activerecord (4.2.7)
+ activemodel (= 4.2.7)
+ activesupport (= 4.2.7)
arel (~> 6.0)
- activesupport (4.2.6)
+ activesupport (4.2.7)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
+ ansi (1.5.0)
arel (6.0.3)
+ better_errors (2.1.1)
+ coderay (>= 1.0.0)
+ erubis (>= 2.6.6)
+ rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.2.2)
- byebug (8.2.5)
+ byebug (9.0.6)
+ coderay (1.1.1)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
@@ -48,46 +54,84 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.10.0)
- concurrent-ruby (1.0.1)
+ concurrent-ruby (1.0.2)
debug_inspector (0.0.2)
+ docile (1.1.5)
+ dotenv (2.1.1)
+ dotenv-rails (2.1.1)
+ dotenv (= 2.1.1)
+ railties (>= 4.0, < 5.1)
erubis (2.7.0)
- execjs (2.6.0)
- globalid (0.3.6)
+ execjs (2.7.0)
+ faraday (0.9.2)
+ multipart-post (>= 1.2, < 3)
+ globalid (0.3.7)
activesupport (>= 4.1.0)
+ hashie (3.4.6)
i18n (0.7.0)
- jbuilder (2.4.1)
+ jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
- jquery-rails (4.1.1)
+ jquery-rails (4.2.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.3)
+ jwt (1.5.6)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mime-types (3.0)
+ method_source (0.8.2)
+ mime-types (3.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0221)
- mini_portile2 (2.0.0)
- minitest (5.8.4)
- multi_json (1.11.3)
- nokogiri (1.6.7.2)
- mini_portile2 (~> 2.0.0.rc2)
+ mime-types-data (3.2016.0521)
+ mini_portile2 (2.1.0)
+ minitest (5.9.1)
+ minitest-reporters (1.1.11)
+ ansi
+ builder
+ minitest (>= 5.0)
+ ruby-progressbar
+ multi_json (1.12.1)
+ multi_xml (0.5.5)
+ multipart-post (2.0.0)
+ nokogiri (1.6.8.1)
+ mini_portile2 (~> 2.1.0)
+ oauth2 (1.2.0)
+ faraday (>= 0.8, < 0.10)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 3)
+ omniauth (1.3.1)
+ hashie (>= 1.2, < 4)
+ rack (>= 1.0, < 3)
+ omniauth-github (1.1.2)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-oauth2 (1.4.0)
+ oauth2 (~> 1.0)
+ omniauth (~> 1.2)
+ pry (0.10.4)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ pry-rails (0.3.4)
+ pry (>= 0.9.10)
rack (1.6.4)
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.6)
- actionmailer (= 4.2.6)
- actionpack (= 4.2.6)
- actionview (= 4.2.6)
- activejob (= 4.2.6)
- activemodel (= 4.2.6)
- activerecord (= 4.2.6)
- activesupport (= 4.2.6)
+ rails (4.2.7)
+ actionmailer (= 4.2.7)
+ actionpack (= 4.2.7)
+ actionview (= 4.2.7)
+ activejob (= 4.2.7)
+ activemodel (= 4.2.7)
+ activerecord (= 4.2.7)
+ activesupport (= 4.2.7)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.6)
+ railties (= 4.2.7)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
@@ -97,40 +141,50 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (4.2.6)
- actionpack (= 4.2.6)
- activesupport (= 4.2.6)
+ railties (4.2.7)
+ actionpack (= 4.2.7)
+ activesupport (= 4.2.7)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rake (11.1.2)
+ rake (11.3.0)
rdoc (4.2.2)
json (~> 1.4)
+ ruby-progressbar (1.8.1)
sass (3.4.22)
- sass-rails (5.0.4)
- railties (>= 4.0.0, < 5.0)
+ sass-rails (5.0.6)
+ railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
- sdoc (0.4.1)
+ sdoc (0.4.2)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
- spring (1.7.1)
- sprockets (3.6.0)
+ simplecov (0.12.0)
+ docile (~> 1.1.0)
+ json (>= 1.8, < 3)
+ simplecov-html (~> 0.10.0)
+ simplecov-html (0.10.0)
+ slop (3.6.0)
+ spring (2.0.0)
+ activesupport (>= 4.2)
+ sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-rails (3.0.4)
+ sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ sqlite3 (1.3.12)
thor (0.19.1)
thread_safe (0.3.5)
- tilt (2.0.2)
- turbolinks (2.5.3)
- coffee-rails
+ tilt (2.0.5)
+ turbolinks (5.0.1)
+ turbolinks-source (~> 5)
+ turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- uglifier (3.0.0)
+ uglifier (3.0.2)
execjs (>= 0.3.0, < 3)
web-console (2.3.0)
activemodel (>= 4.0)
@@ -142,20 +196,26 @@ PLATFORMS
ruby
DEPENDENCIES
+ better_errors
+ binding_of_caller
byebug
coffee-rails (~> 4.1.0)
+ dotenv-rails
jbuilder (~> 2.0)
jquery-rails
- rails (= 4.2.6)
+ minitest-reporters
+ omniauth
+ omniauth-github
+ pry-rails
+ rails (= 4.2.7)
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
+ simplecov
spring
+ sqlite3
turbolinks
uglifier (>= 1.3.0)
web-console (~> 2.0)
-RUBY VERSION
- ruby 2.3.1p112
-
BUNDLED WITH
- 1.13.5
+ 1.13.1
diff --git a/README.md b/README.md
deleted file mode 100644
index 220a0423c4..0000000000
--- a/README.md
+++ /dev/null
@@ -1,194 +0,0 @@
-# bEtsy
-[b]Etsy will be an online store where a wide variety of products can be listed and sold by any user. In this project we will focus on reinforcing the major components of Rails, Model Validation, as well as introducing some more complex logic such as user authentication.
-
-## Project Learning Goals
-- Core comprehension of:
- - Routes
- - Controllers
- - Models
- - Views
-- User based application logic
-- User authentication
-- Agile practices
-- Feature branch management with Git
-- Group project ownership
-
-## Guidelines
-- Groups of three or four will collaborate in pairs or individually and will report to their assigned Project Manager (one of the instructors)
-- Use a task manager like [Trello](http://trello.com) to track your team's efforts
-- Build a logical user-flow that moves across multiple controllers and models
-- Use HTML/CSS to style your website
-
-### Restrictions
-- Do not use gems for user authentication (such as Devise)
-- You must use oAuth for user authentication.
-
-## Getting Started
-1. As a group decide on an app name (this may help lead the aesthetic)
-1. As a group decide on a team name (this will amuse your instructors)
-1. Have one person on your team fork/clone the project master as per usual
- 1. Add all other team members as collaborators
- 1. Each team member should clone the repo to their computer
-1. Figure out your workflow for the project, re: Git and Task management
- 1. Determine who will be the Stand Up Leader and Task Leader for the first week
-1. Create a Trello board and ensure that all team members and instructors have access
-1. Review the User Stories below and create Trello tasks to represent them
-1. Slack your team name, app name, and link to your trello board to your Project Manager
-
-## Expectations
-Build an online system for listing, selling, reviewing, and buying a wide variety of products listed by multiple merchants.
-
-### General Requirements
-- Unit tests and/or specs for
- - Controllers
- - Models
- - Routes
-- Test code coverage (using SimpleCov - remember me!)
- - 90% for all controller and model classes
-
-### User Stories
-#### Guest User (Unauthenticated)
-As a guest to the website (not signed in) I **can**:
-
-- Browse all products
-- Browse products by category
-- Browse products by merchant (users)
-- View any individual product with additional details
-- Leave a review for a product providing:
- - A Text review
- - A rating out of 5
-- Add in-stock products to my cart
-- Remove products from my cart
-- Change the quantity of an existing product in my cart
-- Purchase the items in my cart, providing:
- - Email Address
- - Mailing Address
- - Name on credit card
- - Credit card number
- - Credit cart expiration
- - Credit Card CVV (security code)
- - Billing zip code
-- Purchasing an order makes the following changes:
- - Reduces the number of inventory for each product
- - Changes the order state from "pending" to "paid"
- - Clears the current cart
-- After purchasing an order, I can view a confirmation screen including:
- - Each item in the order with a quantity and line-item subtotal
- - A link to the item description page
- - Order total price
- - DateTime the order was placed
- - The current status of the order
-- Sign up to be a merchant using OAuth
- - Every merchant must have a username
-- Sign in to my merchant account using OAuth
-
-As a guest I **cannot**:
-
-- Add products to the cart that are out of stock
-- View any link or page to manage any products
-- View any of the account pages
-
-#### Authenticated Users
-As a signed-in user, I **can**:
-
-- Do everything a guest user can do except for sign up and sign in
-- Sign out
-- Create new categories (categories are shared between all merchants)
-- Create a new product providing:
- - name
- - description
- - price
- - photo URL
- - stock
-- Assign my products to any number of categories
-- Retire a product from being sold, which hides it from browsing
-- View an account page to edit/update my existing products
-- View an account page showing my order fulfillment
-- On the order fulfillment page:
- - Total Revenue
- - Total Revenue by status
- - Total number of orders by status
- - Filter orders displayed by status
- - Link to each individual order
- - A list of orders including at least one of my products:
- - Each order item sold by me with a quantity and line-item subtotal
- - A link to the item description page
- - DateTime the order was placed
- - Link to transition the order item to marked as shipped
- - The current status of the order ("pending", "paid", "complete", "cancelled")
-- View an individual order to see the user's:
- - Name
- - Email address
- - Mailing address
- - Last four digits of their credit card
- - Credit card expiration date
-
-As a signed-in user, I **cannot**:
-
-- Review my own products
-- View order items from a shared order that belong to another merchant
-- View another users private data (i.e. order fulfillment or product management)
-
-### Model Validations
-Many of our models will have attributes that are required for our application to use and display data consistently. Each model will have attributes with requirements for a valid record. The requirements are summarized below:
-
-#### Merchant
-- Username must be present
-- Username must be unique
-- Email Address must be present
-- Email Address must be unique
-
-#### Product
-- Name must be present
-- Name must be unique
-- Price must be present
-- Price must be a number
-- Price must be greater than 0
-- Product must belong to a User
-
-#### Order
-- An Order must have one or more Order Items
-
-#### OrderItem
-- Must belong to a Product
-- Must belong to an Order
-- Quantity must be present
-- Quantity must be an integer
-- Quantity must be greater than 0
-
-#### Review
-- Rating must be present
-- Rating must be an integer
-- Rating must be between 1 and 5
-
-## Submission Guidelines
-Your final project must be deployed to [Heroku](http://heroku.com). Your team will open a single pull request for the entire project. Include the link to your Heroku deployment in the PR's description, as well as the team name and the names of all members.
-
-## Team Leaders
-Each team will have team leaders who are responsible for keeping track of each team member's contributions. Rotate leader roles at the beginning of each week; every team member should be in at least one leader role during the project.
-
-- Stand Up Leader
- - Notifies team members about meeting schedule and ensures that everyone is present and ready
- - Takes notes about each person's daily report in Stand Up
- - Keeps the meeting moving
-- Task Leader
- - Leads discussion on task assignment
- - Decide if a task should be completed alone or in a pair
- - Assign tasks based on...
- - Individual comfort
- - Desire
- - Ability
- - Ensures the task list stays up to date
-
-## Stand Up Meetings
-The Project Manager for your team will determine the timing for all Stand Up meetings. Because PMs are managing multiple projects at once this time will be different for each team, and may change from day to day. The meeting schedule will be communicated to the Stand Up Leader as soon as it is determined.
-
-At the end of each day, your team's assigned Project Manager will review the Trello board for all tasks discussed during that day's Stand Up meeting.
-
-## Weekly Demos
-In a real world work environment, a team's success is measured by their product as opposed to each individual's contribution.
-
-Each team will present their progress and respond to questions from their Project Manager each Friday. Every team member will participate in these demos; the PM will ask specific questions regarding
-1. The team's progress and plan for completing the project
-1. The technical decisions and implementation
-1. Every team member's understanding of the underlying technical structures.
diff --git a/README.rdoc b/README.rdoc
new file mode 100644
index 0000000000..dd4e97e22e
--- /dev/null
+++ b/README.rdoc
@@ -0,0 +1,28 @@
+== README
+
+This README would normally document whatever steps are necessary to get the
+application up and running.
+
+Things you may want to cover:
+
+* Ruby version
+
+* System dependencies
+
+* Configuration
+
+* Database creation
+
+* Database initialization
+
+* How to run the test suite
+
+* Services (job queues, cache servers, search engines, etc.)
+
+* Deployment instructions
+
+* ...
+
+
+Please feel free to use a different markup language if you do not plan to run
+rake doc:app.
diff --git a/app/assets/images/star_category_image.jpg b/app/assets/images/star_category_image.jpg
new file mode 100644
index 0000000000..65cea42f93
Binary files /dev/null and b/app/assets/images/star_category_image.jpg differ
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index e07c5a830f..a71cf35238 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -13,4 +13,5 @@
//= require jquery
//= require jquery_ujs
//= require turbolinks
+//= require foundation
//= require_tree .
diff --git a/app/assets/javascripts/carts.coffee b/app/assets/javascripts/carts.coffee
new file mode 100644
index 0000000000..24f83d18bb
--- /dev/null
+++ b/app/assets/javascripts/carts.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/categories.coffee b/app/assets/javascripts/categories.coffee
new file mode 100644
index 0000000000..24f83d18bb
--- /dev/null
+++ b/app/assets/javascripts/categories.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/foundation.js b/app/assets/javascripts/foundation.js
new file mode 100644
index 0000000000..87e04589c2
--- /dev/null
+++ b/app/assets/javascripts/foundation.js
@@ -0,0 +1,9069 @@
+!function ($) {
+
+ "use strict";
+
+ var FOUNDATION_VERSION = '6.2.2';
+
+ // Global Foundation object
+ // This is attached to the window, or used as a module for AMD/Browserify
+ var Foundation = {
+ version: FOUNDATION_VERSION,
+
+ /**
+ * Stores initialized plugins.
+ */
+ _plugins: {},
+
+ /**
+ * Stores generated unique ids for plugin instances
+ */
+ _uuids: [],
+
+ /**
+ * Returns a boolean for RTL support
+ */
+ rtl: function () {
+ return $('html').attr('dir') === 'rtl';
+ },
+ /**
+ * Defines a Foundation plugin, adding it to the `Foundation` namespace and the list of plugins to initialize when reflowing.
+ * @param {Object} plugin - The constructor of the plugin.
+ */
+ plugin: function (plugin, name) {
+ // Object key to use when adding to global Foundation object
+ // Examples: Foundation.Reveal, Foundation.OffCanvas
+ var className = name || functionName(plugin);
+ // Object key to use when storing the plugin, also used to create the identifying data attribute for the plugin
+ // Examples: data-reveal, data-off-canvas
+ var attrName = hyphenate(className);
+
+ // Add to the Foundation object and the plugins list (for reflowing)
+ this._plugins[attrName] = this[className] = plugin;
+ },
+ /**
+ * @function
+ * Populates the _uuids array with pointers to each individual plugin instance.
+ * Adds the `zfPlugin` data-attribute to programmatically created plugins to allow use of $(selector).foundation(method) calls.
+ * Also fires the initialization event for each plugin, consolidating repetitive code.
+ * @param {Object} plugin - an instance of a plugin, usually `this` in context.
+ * @param {String} name - the name of the plugin, passed as a camelCased string.
+ * @fires Plugin#init
+ */
+ registerPlugin: function (plugin, name) {
+ var pluginName = name ? hyphenate(name) : functionName(plugin.constructor).toLowerCase();
+ plugin.uuid = this.GetYoDigits(6, pluginName);
+
+ if (!plugin.$element.attr('data-' + pluginName)) {
+ plugin.$element.attr('data-' + pluginName, plugin.uuid);
+ }
+ if (!plugin.$element.data('zfPlugin')) {
+ plugin.$element.data('zfPlugin', plugin);
+ }
+ /**
+ * Fires when the plugin has initialized.
+ * @event Plugin#init
+ */
+ plugin.$element.trigger('init.zf.' + pluginName);
+
+ this._uuids.push(plugin.uuid);
+
+ return;
+ },
+ /**
+ * @function
+ * Removes the plugins uuid from the _uuids array.
+ * Removes the zfPlugin data attribute, as well as the data-plugin-name attribute.
+ * Also fires the destroyed event for the plugin, consolidating repetitive code.
+ * @param {Object} plugin - an instance of a plugin, usually `this` in context.
+ * @fires Plugin#destroyed
+ */
+ unregisterPlugin: function (plugin) {
+ var pluginName = hyphenate(functionName(plugin.$element.data('zfPlugin').constructor));
+
+ this._uuids.splice(this._uuids.indexOf(plugin.uuid), 1);
+ plugin.$element.removeAttr('data-' + pluginName).removeData('zfPlugin')
+ /**
+ * Fires when the plugin has been destroyed.
+ * @event Plugin#destroyed
+ */
+ .trigger('destroyed.zf.' + pluginName);
+ for (var prop in plugin) {
+ plugin[prop] = null; //clean up script to prep for garbage collection.
+ }
+ return;
+ },
+
+ /**
+ * @function
+ * Causes one or more active plugins to re-initialize, resetting event listeners, recalculating positions, etc.
+ * @param {String} plugins - optional string of an individual plugin key, attained by calling `$(element).data('pluginName')`, or string of a plugin class i.e. `'dropdown'`
+ * @default If no argument is passed, reflow all currently active plugins.
+ */
+ reInit: function (plugins) {
+ var isJQ = plugins instanceof $;
+ try {
+ if (isJQ) {
+ plugins.each(function () {
+ $(this).data('zfPlugin')._init();
+ });
+ } else {
+ var type = typeof plugins,
+ _this = this,
+ fns = {
+ 'object': function (plgs) {
+ plgs.forEach(function (p) {
+ p = hyphenate(p);
+ $('[data-' + p + ']').foundation('_init');
+ });
+ },
+ 'string': function () {
+ plugins = hyphenate(plugins);
+ $('[data-' + plugins + ']').foundation('_init');
+ },
+ 'undefined': function () {
+ this['object'](Object.keys(_this._plugins));
+ }
+ };
+ fns[type](plugins);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ return plugins;
+ }
+ },
+
+ /**
+ * returns a random base-36 uid with namespacing
+ * @function
+ * @param {Number} length - number of random base-36 digits desired. Increase for more random strings.
+ * @param {String} namespace - name of plugin to be incorporated in uid, optional.
+ * @default {String} '' - if no plugin name is provided, nothing is appended to the uid.
+ * @returns {String} - unique id
+ */
+ GetYoDigits: function (length, namespace) {
+ length = length || 6;
+ return Math.round(Math.pow(36, length + 1) - Math.random() * Math.pow(36, length)).toString(36).slice(1) + (namespace ? '-' + namespace : '');
+ },
+ /**
+ * Initialize plugins on any elements within `elem` (and `elem` itself) that aren't already initialized.
+ * @param {Object} elem - jQuery object containing the element to check inside. Also checks the element itself, unless it's the `document` object.
+ * @param {String|Array} plugins - A list of plugins to initialize. Leave this out to initialize everything.
+ */
+ reflow: function (elem, plugins) {
+
+ // If plugins is undefined, just grab everything
+ if (typeof plugins === 'undefined') {
+ plugins = Object.keys(this._plugins);
+ }
+ // If plugins is a string, convert it to an array with one item
+ else if (typeof plugins === 'string') {
+ plugins = [plugins];
+ }
+
+ var _this = this;
+
+ // Iterate through each plugin
+ $.each(plugins, function (i, name) {
+ // Get the current plugin
+ var plugin = _this._plugins[name];
+
+ // Localize the search to all elements inside elem, as well as elem itself, unless elem === document
+ var $elem = $(elem).find('[data-' + name + ']').addBack('[data-' + name + ']');
+
+ // For each plugin found, initialize it
+ $elem.each(function () {
+ var $el = $(this),
+ opts = {};
+ // Don't double-dip on plugins
+ if ($el.data('zfPlugin')) {
+ console.warn("Tried to initialize " + name + " on an element that already has a Foundation plugin.");
+ return;
+ }
+
+ if ($el.attr('data-options')) {
+ var thing = $el.attr('data-options').split(';').forEach(function (e, i) {
+ var opt = e.split(':').map(function (el) {
+ return el.trim();
+ });
+ if (opt[0]) opts[opt[0]] = parseValue(opt[1]);
+ });
+ }
+ try {
+ $el.data('zfPlugin', new plugin($(this), opts));
+ } catch (er) {
+ console.error(er);
+ } finally {
+ return;
+ }
+ });
+ });
+ },
+ getFnName: functionName,
+ transitionend: function ($elem) {
+ var transitions = {
+ 'transition': 'transitionend',
+ 'WebkitTransition': 'webkitTransitionEnd',
+ 'MozTransition': 'transitionend',
+ 'OTransition': 'otransitionend'
+ };
+ var elem = document.createElement('div'),
+ end;
+
+ for (var t in transitions) {
+ if (typeof elem.style[t] !== 'undefined') {
+ end = transitions[t];
+ }
+ }
+ if (end) {
+ return end;
+ } else {
+ end = setTimeout(function () {
+ $elem.triggerHandler('transitionend', [$elem]);
+ }, 1);
+ return 'transitionend';
+ }
+ }
+ };
+
+ Foundation.util = {
+ /**
+ * Function for applying a debounce effect to a function call.
+ * @function
+ * @param {Function} func - Function to be called at end of timeout.
+ * @param {Number} delay - Time in ms to delay the call of `func`.
+ * @returns function
+ */
+ throttle: function (func, delay) {
+ var timer = null;
+
+ return function () {
+ var context = this,
+ args = arguments;
+
+ if (timer === null) {
+ timer = setTimeout(function () {
+ func.apply(context, args);
+ timer = null;
+ }, delay);
+ }
+ };
+ }
+ };
+
+ // TODO: consider not making this a jQuery function
+ // TODO: need way to reflow vs. re-initialize
+ /**
+ * The Foundation jQuery method.
+ * @param {String|Array} method - An action to perform on the current jQuery object.
+ */
+ var foundation = function (method) {
+ var type = typeof method,
+ $meta = $('meta.foundation-mq'),
+ $noJS = $('.no-js');
+
+ if (!$meta.length) {
+ $('').appendTo(document.head);
+ }
+ if ($noJS.length) {
+ $noJS.removeClass('no-js');
+ }
+
+ if (type === 'undefined') {
+ //needs to initialize the Foundation object, or an individual plugin.
+ Foundation.MediaQuery._init();
+ Foundation.reflow(this);
+ } else if (type === 'string') {
+ //an individual method to invoke on a plugin or group of plugins
+ var args = Array.prototype.slice.call(arguments, 1); //collect all the arguments, if necessary
+ var plugClass = this.data('zfPlugin'); //determine the class of plugin
+
+ if (plugClass !== undefined && plugClass[method] !== undefined) {
+ //make sure both the class and method exist
+ if (this.length === 1) {
+ //if there's only one, call it directly.
+ plugClass[method].apply(plugClass, args);
+ } else {
+ this.each(function (i, el) {
+ //otherwise loop through the jQuery collection and invoke the method on each
+ plugClass[method].apply($(el).data('zfPlugin'), args);
+ });
+ }
+ } else {
+ //error for no class or no method
+ throw new ReferenceError("We're sorry, '" + method + "' is not an available method for " + (plugClass ? functionName(plugClass) : 'this element') + '.');
+ }
+ } else {
+ //error for invalid argument type
+ throw new TypeError('We\'re sorry, ' + type + ' is not a valid parameter. You must use a string representing the method you wish to invoke.');
+ }
+ return this;
+ };
+
+ window.Foundation = Foundation;
+ $.fn.foundation = foundation;
+
+ // Polyfill for requestAnimationFrame
+ (function () {
+ if (!Date.now || !window.Date.now) window.Date.now = Date.now = function () {
+ return new Date().getTime();
+ };
+
+ var vendors = ['webkit', 'moz'];
+ for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
+ var vp = vendors[i];
+ window.requestAnimationFrame = window[vp + 'RequestAnimationFrame'];
+ window.cancelAnimationFrame = window[vp + 'CancelAnimationFrame'] || window[vp + 'CancelRequestAnimationFrame'];
+ }
+ if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) || !window.requestAnimationFrame || !window.cancelAnimationFrame) {
+ var lastTime = 0;
+ window.requestAnimationFrame = function (callback) {
+ var now = Date.now();
+ var nextTime = Math.max(lastTime + 16, now);
+ return setTimeout(function () {
+ callback(lastTime = nextTime);
+ }, nextTime - now);
+ };
+ window.cancelAnimationFrame = clearTimeout;
+ }
+ /**
+ * Polyfill for performance.now, required by rAF
+ */
+ if (!window.performance || !window.performance.now) {
+ window.performance = {
+ start: Date.now(),
+ now: function () {
+ return Date.now() - this.start;
+ }
+ };
+ }
+ })();
+ if (!Function.prototype.bind) {
+ Function.prototype.bind = function (oThis) {
+ if (typeof this !== 'function') {
+ // closest thing possible to the ECMAScript 5
+ // internal IsCallable function
+ throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function () {},
+ fBound = function () {
+ return fToBind.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ if (this.prototype) {
+ // native functions don't have a prototype
+ fNOP.prototype = this.prototype;
+ }
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+ }
+ // Polyfill to get the name of a function in IE9
+ function functionName(fn) {
+ if (Function.prototype.name === undefined) {
+ var funcNameRegex = /function\s([^(]{1,})\(/;
+ var results = funcNameRegex.exec(fn.toString());
+ return results && results.length > 1 ? results[1].trim() : "";
+ } else if (fn.prototype === undefined) {
+ return fn.constructor.name;
+ } else {
+ return fn.prototype.constructor.name;
+ }
+ }
+ function parseValue(str) {
+ if (/true/.test(str)) return true;else if (/false/.test(str)) return false;else if (!isNaN(str * 1)) return parseFloat(str);
+ return str;
+ }
+ // Convert PascalCase to kebab-case
+ // Thank you: http://stackoverflow.com/a/8955580
+ function hyphenate(str) {
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ }
+}(jQuery);
+'use strict';
+
+!function ($) {
+
+ // Default set of media queries
+ var defaultQueries = {
+ 'default': 'only screen',
+ landscape: 'only screen and (orientation: landscape)',
+ portrait: 'only screen and (orientation: portrait)',
+ retina: 'only screen and (-webkit-min-device-pixel-ratio: 2),' + 'only screen and (min--moz-device-pixel-ratio: 2),' + 'only screen and (-o-min-device-pixel-ratio: 2/1),' + 'only screen and (min-device-pixel-ratio: 2),' + 'only screen and (min-resolution: 192dpi),' + 'only screen and (min-resolution: 2dppx)'
+ };
+
+ var MediaQuery = {
+ queries: [],
+
+ current: '',
+
+ /**
+ * Initializes the media query helper, by extracting the breakpoint list from the CSS and activating the breakpoint watcher.
+ * @function
+ * @private
+ */
+ _init: function () {
+ var self = this;
+ var extractedStyles = $('.foundation-mq').css('font-family');
+ var namedQueries;
+
+ namedQueries = parseStyleToObject(extractedStyles);
+
+ for (var key in namedQueries) {
+ if (namedQueries.hasOwnProperty(key)) {
+ self.queries.push({
+ name: key,
+ value: 'only screen and (min-width: ' + namedQueries[key] + ')'
+ });
+ }
+ }
+
+ this.current = this._getCurrentSize();
+
+ this._watcher();
+ },
+
+
+ /**
+ * Checks if the screen is at least as wide as a breakpoint.
+ * @function
+ * @param {String} size - Name of the breakpoint to check.
+ * @returns {Boolean} `true` if the breakpoint matches, `false` if it's smaller.
+ */
+ atLeast: function (size) {
+ var query = this.get(size);
+
+ if (query) {
+ return window.matchMedia(query).matches;
+ }
+
+ return false;
+ },
+
+
+ /**
+ * Gets the media query of a breakpoint.
+ * @function
+ * @param {String} size - Name of the breakpoint to get.
+ * @returns {String|null} - The media query of the breakpoint, or `null` if the breakpoint doesn't exist.
+ */
+ get: function (size) {
+ for (var i in this.queries) {
+ if (this.queries.hasOwnProperty(i)) {
+ var query = this.queries[i];
+ if (size === query.name) return query.value;
+ }
+ }
+
+ return null;
+ },
+
+
+ /**
+ * Gets the current breakpoint name by testing every breakpoint and returning the last one to match (the biggest one).
+ * @function
+ * @private
+ * @returns {String} Name of the current breakpoint.
+ */
+ _getCurrentSize: function () {
+ var matched;
+
+ for (var i = 0; i < this.queries.length; i++) {
+ var query = this.queries[i];
+
+ if (window.matchMedia(query.value).matches) {
+ matched = query;
+ }
+ }
+
+ if (typeof matched === 'object') {
+ return matched.name;
+ } else {
+ return matched;
+ }
+ },
+
+
+ /**
+ * Activates the breakpoint watcher, which fires an event on the window whenever the breakpoint changes.
+ * @function
+ * @private
+ */
+ _watcher: function () {
+ var _this = this;
+
+ $(window).on('resize.zf.mediaquery', function () {
+ var newSize = _this._getCurrentSize(),
+ currentSize = _this.current;
+
+ if (newSize !== currentSize) {
+ // Change the current media query
+ _this.current = newSize;
+
+ // Broadcast the media query change on the window
+ $(window).trigger('changed.zf.mediaquery', [newSize, currentSize]);
+ }
+ });
+ }
+ };
+
+ Foundation.MediaQuery = MediaQuery;
+
+ // matchMedia() polyfill - Test a CSS media type/query in JS.
+ // Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license
+ window.matchMedia || (window.matchMedia = function () {
+ 'use strict';
+
+ // For browsers that support matchMedium api such as IE 9 and webkit
+
+ var styleMedia = window.styleMedia || window.media;
+
+ // For those that don't support matchMedium
+ if (!styleMedia) {
+ var style = document.createElement('style'),
+ script = document.getElementsByTagName('script')[0],
+ info = null;
+
+ style.type = 'text/css';
+ style.id = 'matchmediajs-test';
+
+ script.parentNode.insertBefore(style, script);
+
+ // 'style.currentStyle' is used by IE <= 8 and 'window.getComputedStyle' for all other browsers
+ info = 'getComputedStyle' in window && window.getComputedStyle(style, null) || style.currentStyle;
+
+ styleMedia = {
+ matchMedium: function (media) {
+ var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }';
+
+ // 'style.styleSheet' is used by IE <= 8 and 'style.textContent' for all other browsers
+ if (style.styleSheet) {
+ style.styleSheet.cssText = text;
+ } else {
+ style.textContent = text;
+ }
+
+ // Test if media query is true or false
+ return info.width === '1px';
+ }
+ };
+ }
+
+ return function (media) {
+ return {
+ matches: styleMedia.matchMedium(media || 'all'),
+ media: media || 'all'
+ };
+ };
+ }());
+
+ // Thank you: https://github.com/sindresorhus/query-string
+ function parseStyleToObject(str) {
+ var styleObject = {};
+
+ if (typeof str !== 'string') {
+ return styleObject;
+ }
+
+ str = str.trim().slice(1, -1); // browsers re-quote string style values
+
+ if (!str) {
+ return styleObject;
+ }
+
+ styleObject = str.split('&').reduce(function (ret, param) {
+ var parts = param.replace(/\+/g, ' ').split('=');
+ var key = parts[0];
+ var val = parts[1];
+ key = decodeURIComponent(key);
+
+ // missing `=` should be `null`:
+ // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
+ val = val === undefined ? null : decodeURIComponent(val);
+
+ if (!ret.hasOwnProperty(key)) {
+ ret[key] = val;
+ } else if (Array.isArray(ret[key])) {
+ ret[key].push(val);
+ } else {
+ ret[key] = [ret[key], val];
+ }
+ return ret;
+ }, {});
+
+ return styleObject;
+ }
+
+ Foundation.MediaQuery = MediaQuery;
+}(jQuery);
+/*******************************************
+ * *
+ * This util was created by Marius Olbertz *
+ * Please thank Marius on GitHub /owlbertz *
+ * or the web http://www.mariusolbertz.de/ *
+ * *
+ ******************************************/
+
+'use strict';
+
+!function ($) {
+
+ var keyCodes = {
+ 9: 'TAB',
+ 13: 'ENTER',
+ 27: 'ESCAPE',
+ 32: 'SPACE',
+ 37: 'ARROW_LEFT',
+ 38: 'ARROW_UP',
+ 39: 'ARROW_RIGHT',
+ 40: 'ARROW_DOWN'
+ };
+
+ var commands = {};
+
+ var Keyboard = {
+ keys: getKeyCodes(keyCodes),
+
+ /**
+ * Parses the (keyboard) event and returns a String that represents its key
+ * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
+ * @param {Event} event - the event generated by the event handler
+ * @return String key - String that represents the key pressed
+ */
+ parseKey: function (event) {
+ var key = keyCodes[event.which || event.keyCode] || String.fromCharCode(event.which).toUpperCase();
+ if (event.shiftKey) key = 'SHIFT_' + key;
+ if (event.ctrlKey) key = 'CTRL_' + key;
+ if (event.altKey) key = 'ALT_' + key;
+ return key;
+ },
+
+
+ /**
+ * Handles the given (keyboard) event
+ * @param {Event} event - the event generated by the event handler
+ * @param {String} component - Foundation component's name, e.g. Slider or Reveal
+ * @param {Objects} functions - collection of functions that are to be executed
+ */
+ handleKey: function (event, component, functions) {
+ var commandList = commands[component],
+ keyCode = this.parseKey(event),
+ cmds,
+ command,
+ fn;
+
+ if (!commandList) return console.warn('Component not defined!');
+
+ if (typeof commandList.ltr === 'undefined') {
+ // this component does not differentiate between ltr and rtl
+ cmds = commandList; // use plain list
+ } else {
+ // merge ltr and rtl: if document is rtl, rtl overwrites ltr and vice versa
+ if (Foundation.rtl()) cmds = $.extend({}, commandList.ltr, commandList.rtl);else cmds = $.extend({}, commandList.rtl, commandList.ltr);
+ }
+ command = cmds[keyCode];
+
+ fn = functions[command];
+ if (fn && typeof fn === 'function') {
+ // execute function if exists
+ var returnValue = fn.apply();
+ if (functions.handled || typeof functions.handled === 'function') {
+ // execute function when event was handled
+ functions.handled(returnValue);
+ }
+ } else {
+ if (functions.unhandled || typeof functions.unhandled === 'function') {
+ // execute function when event was not handled
+ functions.unhandled();
+ }
+ }
+ },
+
+
+ /**
+ * Finds all focusable elements within the given `$element`
+ * @param {jQuery} $element - jQuery object to search within
+ * @return {jQuery} $focusable - all focusable elements within `$element`
+ */
+ findFocusable: function ($element) {
+ return $element.find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]').filter(function () {
+ if (!$(this).is(':visible') || $(this).attr('tabindex') < 0) {
+ return false;
+ } //only have visible elements and those that have a tabindex greater or equal 0
+ return true;
+ });
+ },
+
+
+ /**
+ * Returns the component name name
+ * @param {Object} component - Foundation component, e.g. Slider or Reveal
+ * @return String componentName
+ */
+
+ register: function (componentName, cmds) {
+ commands[componentName] = cmds;
+ }
+ };
+
+ /*
+ * Constants for easier comparing.
+ * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
+ */
+ function getKeyCodes(kcs) {
+ var k = {};
+ for (var kc in kcs) {
+ k[kcs[kc]] = kcs[kc];
+ }return k;
+ }
+
+ Foundation.Keyboard = Keyboard;
+}(jQuery);
+'use strict';
+
+!function ($) {
+
+ /**
+ * Motion module.
+ * @module foundation.motion
+ */
+
+ var initClasses = ['mui-enter', 'mui-leave'];
+ var activeClasses = ['mui-enter-active', 'mui-leave-active'];
+
+ var Motion = {
+ animateIn: function (element, animation, cb) {
+ animate(true, element, animation, cb);
+ },
+
+ animateOut: function (element, animation, cb) {
+ animate(false, element, animation, cb);
+ }
+ };
+
+ function Move(duration, elem, fn) {
+ var anim,
+ prog,
+ start = null;
+ // console.log('called');
+
+ function move(ts) {
+ if (!start) start = window.performance.now();
+ // console.log(start, ts);
+ prog = ts - start;
+ fn.apply(elem);
+
+ if (prog < duration) {
+ anim = window.requestAnimationFrame(move, elem);
+ } else {
+ window.cancelAnimationFrame(anim);
+ elem.trigger('finished.zf.animate', [elem]).triggerHandler('finished.zf.animate', [elem]);
+ }
+ }
+ anim = window.requestAnimationFrame(move);
+ }
+
+ /**
+ * Animates an element in or out using a CSS transition class.
+ * @function
+ * @private
+ * @param {Boolean} isIn - Defines if the animation is in or out.
+ * @param {Object} element - jQuery or HTML object to animate.
+ * @param {String} animation - CSS class to use.
+ * @param {Function} cb - Callback to run when animation is finished.
+ */
+ function animate(isIn, element, animation, cb) {
+ element = $(element).eq(0);
+
+ if (!element.length) return;
+
+ var initClass = isIn ? initClasses[0] : initClasses[1];
+ var activeClass = isIn ? activeClasses[0] : activeClasses[1];
+
+ // Set up the animation
+ reset();
+
+ element.addClass(animation).css('transition', 'none');
+
+ requestAnimationFrame(function () {
+ element.addClass(initClass);
+ if (isIn) element.show();
+ });
+
+ // Start the animation
+ requestAnimationFrame(function () {
+ element[0].offsetWidth;
+ element.css('transition', '').addClass(activeClass);
+ });
+
+ // Clean up the animation when it finishes
+ element.one(Foundation.transitionend(element), finish);
+
+ // Hides the element (for out animations), resets the element, and runs a callback
+ function finish() {
+ if (!isIn) element.hide();
+ reset();
+ if (cb) cb.apply(element);
+ }
+
+ // Resets transitions and removes motion-specific classes
+ function reset() {
+ element[0].style.transitionDuration = 0;
+ element.removeClass(initClass + ' ' + activeClass + ' ' + animation);
+ }
+ }
+
+ Foundation.Move = Move;
+ Foundation.Motion = Motion;
+}(jQuery);
+'use strict';
+
+!function ($) {
+
+ var Nest = {
+ Feather: function (menu) {
+ var type = arguments.length <= 1 || arguments[1] === undefined ? 'zf' : arguments[1];
+
+ menu.attr('role', 'menubar');
+
+ var items = menu.find('li').attr({ 'role': 'menuitem' }),
+ subMenuClass = 'is-' + type + '-submenu',
+ subItemClass = subMenuClass + '-item',
+ hasSubClass = 'is-' + type + '-submenu-parent';
+
+ menu.find('a:first').attr('tabindex', 0);
+
+ items.each(function () {
+ var $item = $(this),
+ $sub = $item.children('ul');
+
+ if ($sub.length) {
+ $item.addClass(hasSubClass).attr({
+ 'aria-haspopup': true,
+ 'aria-expanded': false,
+ 'aria-label': $item.children('a:first').text()
+ });
+
+ $sub.addClass('submenu ' + subMenuClass).attr({
+ 'data-submenu': '',
+ 'aria-hidden': true,
+ 'role': 'menu'
+ });
+ }
+
+ if ($item.parent('[data-submenu]').length) {
+ $item.addClass('is-submenu-item ' + subItemClass);
+ }
+ });
+
+ return;
+ },
+ Burn: function (menu, type) {
+ var items = menu.find('li').removeAttr('tabindex'),
+ subMenuClass = 'is-' + type + '-submenu',
+ subItemClass = subMenuClass + '-item',
+ hasSubClass = 'is-' + type + '-submenu-parent';
+
+ menu.find('*').removeClass(subMenuClass + ' ' + subItemClass + ' ' + hasSubClass + ' is-submenu-item submenu is-active').removeAttr('data-submenu').css('display', '');
+
+ // console.log( menu.find('.' + subMenuClass + ', .' + subItemClass + ', .has-submenu, .is-submenu-item, .submenu, [data-submenu]')
+ // .removeClass(subMenuClass + ' ' + subItemClass + ' has-submenu is-submenu-item submenu')
+ // .removeAttr('data-submenu'));
+ // items.each(function(){
+ // var $item = $(this),
+ // $sub = $item.children('ul');
+ // if($item.parent('[data-submenu]').length){
+ // $item.removeClass('is-submenu-item ' + subItemClass);
+ // }
+ // if($sub.length){
+ // $item.removeClass('has-submenu');
+ // $sub.removeClass('submenu ' + subMenuClass).removeAttr('data-submenu');
+ // }
+ // });
+ }
+ };
+
+ Foundation.Nest = Nest;
+}(jQuery);
+'use strict';
+
+!function ($) {
+
+ Foundation.Box = {
+ ImNotTouchingYou: ImNotTouchingYou,
+ GetDimensions: GetDimensions,
+ GetOffsets: GetOffsets
+ };
+
+ /**
+ * Compares the dimensions of an element to a container and determines collision events with container.
+ * @function
+ * @param {jQuery} element - jQuery object to test for collisions.
+ * @param {jQuery} parent - jQuery object to use as bounding container.
+ * @param {Boolean} lrOnly - set to true to check left and right values only.
+ * @param {Boolean} tbOnly - set to true to check top and bottom values only.
+ * @default if no parent object passed, detects collisions with `window`.
+ * @returns {Boolean} - true if collision free, false if a collision in any direction.
+ */
+ function ImNotTouchingYou(element, parent, lrOnly, tbOnly) {
+ var eleDims = GetDimensions(element),
+ top,
+ bottom,
+ left,
+ right;
+
+ if (parent) {
+ var parDims = GetDimensions(parent);
+
+ bottom = eleDims.offset.top + eleDims.height <= parDims.height + parDims.offset.top;
+ top = eleDims.offset.top >= parDims.offset.top;
+ left = eleDims.offset.left >= parDims.offset.left;
+ right = eleDims.offset.left + eleDims.width <= parDims.width + parDims.offset.left;
+ } else {
+ bottom = eleDims.offset.top + eleDims.height <= eleDims.windowDims.height + eleDims.windowDims.offset.top;
+ top = eleDims.offset.top >= eleDims.windowDims.offset.top;
+ left = eleDims.offset.left >= eleDims.windowDims.offset.left;
+ right = eleDims.offset.left + eleDims.width <= eleDims.windowDims.width;
+ }
+
+ var allDirs = [bottom, top, left, right];
+
+ if (lrOnly) {
+ return left === right === true;
+ }
+
+ if (tbOnly) {
+ return top === bottom === true;
+ }
+
+ return allDirs.indexOf(false) === -1;
+ };
+
+ /**
+ * Uses native methods to return an object of dimension values.
+ * @function
+ * @param {jQuery || HTML} element - jQuery object or DOM element for which to get the dimensions. Can be any element other that document or window.
+ * @returns {Object} - nested object of integer pixel values
+ * TODO - if element is window, return only those values.
+ */
+ function GetDimensions(elem, test) {
+ elem = elem.length ? elem[0] : elem;
+
+ if (elem === window || elem === document) {
+ throw new Error("I'm sorry, Dave. I'm afraid I can't do that.");
+ }
+
+ var rect = elem.getBoundingClientRect(),
+ parRect = elem.parentNode.getBoundingClientRect(),
+ winRect = document.body.getBoundingClientRect(),
+ winY = window.pageYOffset,
+ winX = window.pageXOffset;
+
+ return {
+ width: rect.width,
+ height: rect.height,
+ offset: {
+ top: rect.top + winY,
+ left: rect.left + winX
+ },
+ parentDims: {
+ width: parRect.width,
+ height: parRect.height,
+ offset: {
+ top: parRect.top + winY,
+ left: parRect.left + winX
+ }
+ },
+ windowDims: {
+ width: winRect.width,
+ height: winRect.height,
+ offset: {
+ top: winY,
+ left: winX
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns an object of top and left integer pixel values for dynamically rendered elements,
+ * such as: Tooltip, Reveal, and Dropdown
+ * @function
+ * @param {jQuery} element - jQuery object for the element being positioned.
+ * @param {jQuery} anchor - jQuery object for the element's anchor point.
+ * @param {String} position - a string relating to the desired position of the element, relative to it's anchor
+ * @param {Number} vOffset - integer pixel value of desired vertical separation between anchor and element.
+ * @param {Number} hOffset - integer pixel value of desired horizontal separation between anchor and element.
+ * @param {Boolean} isOverflow - if a collision event is detected, sets to true to default the element to full width - any desired offset.
+ * TODO alter/rewrite to work with `em` values as well/instead of pixels
+ */
+ function GetOffsets(element, anchor, position, vOffset, hOffset, isOverflow) {
+ var $eleDims = GetDimensions(element),
+ $anchorDims = anchor ? GetDimensions(anchor) : null;
+
+ switch (position) {
+ case 'top':
+ return {
+ left: Foundation.rtl() ? $anchorDims.offset.left - $eleDims.width + $anchorDims.width : $anchorDims.offset.left,
+ top: $anchorDims.offset.top - ($eleDims.height + vOffset)
+ };
+ break;
+ case 'left':
+ return {
+ left: $anchorDims.offset.left - ($eleDims.width + hOffset),
+ top: $anchorDims.offset.top
+ };
+ break;
+ case 'right':
+ return {
+ left: $anchorDims.offset.left + $anchorDims.width + hOffset,
+ top: $anchorDims.offset.top
+ };
+ break;
+ case 'center top':
+ return {
+ left: $anchorDims.offset.left + $anchorDims.width / 2 - $eleDims.width / 2,
+ top: $anchorDims.offset.top - ($eleDims.height + vOffset)
+ };
+ break;
+ case 'center bottom':
+ return {
+ left: isOverflow ? hOffset : $anchorDims.offset.left + $anchorDims.width / 2 - $eleDims.width / 2,
+ top: $anchorDims.offset.top + $anchorDims.height + vOffset
+ };
+ break;
+ case 'center left':
+ return {
+ left: $anchorDims.offset.left - ($eleDims.width + hOffset),
+ top: $anchorDims.offset.top + $anchorDims.height / 2 - $eleDims.height / 2
+ };
+ break;
+ case 'center right':
+ return {
+ left: $anchorDims.offset.left + $anchorDims.width + hOffset + 1,
+ top: $anchorDims.offset.top + $anchorDims.height / 2 - $eleDims.height / 2
+ };
+ break;
+ case 'center':
+ return {
+ left: $eleDims.windowDims.offset.left + $eleDims.windowDims.width / 2 - $eleDims.width / 2,
+ top: $eleDims.windowDims.offset.top + $eleDims.windowDims.height / 2 - $eleDims.height / 2
+ };
+ break;
+ case 'reveal':
+ return {
+ left: ($eleDims.windowDims.width - $eleDims.width) / 2,
+ top: $eleDims.windowDims.offset.top + vOffset
+ };
+ case 'reveal full':
+ return {
+ left: $eleDims.windowDims.offset.left,
+ top: $eleDims.windowDims.offset.top
+ };
+ break;
+ case 'left bottom':
+ return {
+ left: $anchorDims.offset.left - ($eleDims.width + hOffset),
+ top: $anchorDims.offset.top + $anchorDims.height
+ };
+ break;
+ case 'right bottom':
+ return {
+ left: $anchorDims.offset.left + $anchorDims.width + hOffset - $eleDims.width,
+ top: $anchorDims.offset.top + $anchorDims.height
+ };
+ break;
+ default:
+ return {
+ left: Foundation.rtl() ? $anchorDims.offset.left - $eleDims.width + $anchorDims.width : $anchorDims.offset.left,
+ top: $anchorDims.offset.top + $anchorDims.height + vOffset
+ };
+ }
+ }
+}(jQuery);
+'use strict';
+
+!function ($) {
+
+ var MutationObserver = function () {
+ var prefixes = ['WebKit', 'Moz', 'O', 'Ms', ''];
+ for (var i = 0; i < prefixes.length; i++) {
+ if (prefixes[i] + 'MutationObserver' in window) {
+ return window[prefixes[i] + 'MutationObserver'];
+ }
+ }
+ return false;
+ }();
+
+ var triggers = function (el, type) {
+ el.data(type).split(' ').forEach(function (id) {
+ $('#' + id)[type === 'close' ? 'trigger' : 'triggerHandler'](type + '.zf.trigger', [el]);
+ });
+ };
+ // Elements with [data-open] will reveal a plugin that supports it when clicked.
+ $(document).on('click.zf.trigger', '[data-open]', function () {
+ triggers($(this), 'open');
+ });
+
+ // Elements with [data-close] will close a plugin that supports it when clicked.
+ // If used without a value on [data-close], the event will bubble, allowing it to close a parent component.
+ $(document).on('click.zf.trigger', '[data-close]', function () {
+ var id = $(this).data('close');
+ if (id) {
+ triggers($(this), 'close');
+ } else {
+ $(this).trigger('close.zf.trigger');
+ }
+ });
+
+ // Elements with [data-toggle] will toggle a plugin that supports it when clicked.
+ $(document).on('click.zf.trigger', '[data-toggle]', function () {
+ triggers($(this), 'toggle');
+ });
+
+ // Elements with [data-closable] will respond to close.zf.trigger events.
+ $(document).on('close.zf.trigger', '[data-closable]', function (e) {
+ e.stopPropagation();
+ var animation = $(this).data('closable');
+
+ if (animation !== '') {
+ Foundation.Motion.animateOut($(this), animation, function () {
+ $(this).trigger('closed.zf');
+ });
+ } else {
+ $(this).fadeOut().trigger('closed.zf');
+ }
+ });
+
+ $(document).on('focus.zf.trigger blur.zf.trigger', '[data-toggle-focus]', function () {
+ var id = $(this).data('toggle-focus');
+ $('#' + id).triggerHandler('toggle.zf.trigger', [$(this)]);
+ });
+
+ /**
+ * Fires once after all other scripts have loaded
+ * @function
+ * @private
+ */
+ $(window).load(function () {
+ checkListeners();
+ });
+
+ function checkListeners() {
+ eventsListener();
+ resizeListener();
+ scrollListener();
+ closemeListener();
+ }
+
+ //******** only fires this function once on load, if there's something to watch ********
+ function closemeListener(pluginName) {
+ var yetiBoxes = $('[data-yeti-box]'),
+ plugNames = ['dropdown', 'tooltip', 'reveal'];
+
+ if (pluginName) {
+ if (typeof pluginName === 'string') {
+ plugNames.push(pluginName);
+ } else if (typeof pluginName === 'object' && typeof pluginName[0] === 'string') {
+ plugNames.concat(pluginName);
+ } else {
+ console.error('Plugin names must be strings');
+ }
+ }
+ if (yetiBoxes.length) {
+ var listeners = plugNames.map(function (name) {
+ return 'closeme.zf.' + name;
+ }).join(' ');
+
+ $(window).off(listeners).on(listeners, function (e, pluginId) {
+ var plugin = e.namespace.split('.')[0];
+ var plugins = $('[data-' + plugin + ']').not('[data-yeti-box="' + pluginId + '"]');
+
+ plugins.each(function () {
+ var _this = $(this);
+
+ _this.triggerHandler('close.zf.trigger', [_this]);
+ });
+ });
+ }
+ }
+
+ function resizeListener(debounce) {
+ var timer = void 0,
+ $nodes = $('[data-resize]');
+ if ($nodes.length) {
+ $(window).off('resize.zf.trigger').on('resize.zf.trigger', function (e) {
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(function () {
+
+ if (!MutationObserver) {
+ //fallback for IE 9
+ $nodes.each(function () {
+ $(this).triggerHandler('resizeme.zf.trigger');
+ });
+ }
+ //trigger all listening elements and signal a resize event
+ $nodes.attr('data-events', "resize");
+ }, debounce || 10); //default time to emit resize event
+ });
+ }
+ }
+
+ function scrollListener(debounce) {
+ var timer = void 0,
+ $nodes = $('[data-scroll]');
+ if ($nodes.length) {
+ $(window).off('scroll.zf.trigger').on('scroll.zf.trigger', function (e) {
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(function () {
+
+ if (!MutationObserver) {
+ //fallback for IE 9
+ $nodes.each(function () {
+ $(this).triggerHandler('scrollme.zf.trigger');
+ });
+ }
+ //trigger all listening elements and signal a scroll event
+ $nodes.attr('data-events', "scroll");
+ }, debounce || 10); //default time to emit scroll event
+ });
+ }
+ }
+
+ function eventsListener() {
+ if (!MutationObserver) {
+ return false;
+ }
+ var nodes = document.querySelectorAll('[data-resize], [data-scroll], [data-mutate]');
+
+ //element callback
+ var listeningElementsMutation = function (mutationRecordsList) {
+ var $target = $(mutationRecordsList[0].target);
+ //trigger the event handler for the element depending on type
+ switch ($target.attr("data-events")) {
+
+ case "resize":
+ $target.triggerHandler('resizeme.zf.trigger', [$target]);
+ break;
+
+ case "scroll":
+ $target.triggerHandler('scrollme.zf.trigger', [$target, window.pageYOffset]);
+ break;
+
+ // case "mutate" :
+ // console.log('mutate', $target);
+ // $target.triggerHandler('mutate.zf.trigger');
+ //
+ // //make sure we don't get stuck in an infinite loop from sloppy codeing
+ // if ($target.index('[data-mutate]') == $("[data-mutate]").length-1) {
+ // domMutationObserver();
+ // }
+ // break;
+
+ default:
+ return false;
+ //nothing
+ }
+ };
+
+ if (nodes.length) {
+ //for each element that needs to listen for resizing, scrolling, (or coming soon mutation) add a single observer
+ for (var i = 0; i <= nodes.length - 1; i++) {
+ var elementObserver = new MutationObserver(listeningElementsMutation);
+ elementObserver.observe(nodes[i], { attributes: true, childList: false, characterData: false, subtree: false, attributeFilter: ["data-events"] });
+ }
+ }
+ }
+
+ // ------------------------------------
+
+ // [PH]
+ // Foundation.CheckWatchers = checkWatchers;
+ Foundation.IHearYou = checkListeners;
+ // Foundation.ISeeYou = scrollListener;
+ // Foundation.IFeelYou = closemeListener;
+}(jQuery);
+
+// function domMutationObserver(debounce) {
+// // !!! This is coming soon and needs more work; not active !!! //
+// var timer,
+// nodes = document.querySelectorAll('[data-mutate]');
+// //
+// if (nodes.length) {
+// // var MutationObserver = (function () {
+// // var prefixes = ['WebKit', 'Moz', 'O', 'Ms', ''];
+// // for (var i=0; i < prefixes.length; i++) {
+// // if (prefixes[i] + 'MutationObserver' in window) {
+// // return window[prefixes[i] + 'MutationObserver'];
+// // }
+// // }
+// // return false;
+// // }());
+//
+//
+// //for the body, we need to listen for all changes effecting the style and class attributes
+// var bodyObserver = new MutationObserver(bodyMutation);
+// bodyObserver.observe(document.body, { attributes: true, childList: true, characterData: false, subtree:true, attributeFilter:["style", "class"]});
+//
+//
+// //body callback
+// function bodyMutation(mutate) {
+// //trigger all listening elements and signal a mutation event
+// if (timer) { clearTimeout(timer); }
+//
+// timer = setTimeout(function() {
+// bodyObserver.disconnect();
+// $('[data-mutate]').attr('data-events',"mutate");
+// }, debounce || 150);
+// }
+// }
+// }
+'use strict';
+
+!function ($) {
+
+ function Timer(elem, options, cb) {
+ var _this = this,
+ duration = options.duration,
+ //options is an object for easily adding features later.
+ nameSpace = Object.keys(elem.data())[0] || 'timer',
+ remain = -1,
+ start,
+ timer;
+
+ this.isPaused = false;
+
+ this.restart = function () {
+ remain = -1;
+ clearTimeout(timer);
+ this.start();
+ };
+
+ this.start = function () {
+ this.isPaused = false;
+ // if(!elem.data('paused')){ return false; }//maybe implement this sanity check if used for other things.
+ clearTimeout(timer);
+ remain = remain <= 0 ? duration : remain;
+ elem.data('paused', false);
+ start = Date.now();
+ timer = setTimeout(function () {
+ if (options.infinite) {
+ _this.restart(); //rerun the timer.
+ }
+ cb();
+ }, remain);
+ elem.trigger('timerstart.zf.' + nameSpace);
+ };
+
+ this.pause = function () {
+ this.isPaused = true;
+ //if(elem.data('paused')){ return false; }//maybe implement this sanity check if used for other things.
+ clearTimeout(timer);
+ elem.data('paused', true);
+ var end = Date.now();
+ remain = remain - (end - start);
+ elem.trigger('timerpaused.zf.' + nameSpace);
+ };
+ }
+
+ /**
+ * Runs a callback function when images are fully loaded.
+ * @param {Object} images - Image(s) to check if loaded.
+ * @param {Func} callback - Function to execute when image is fully loaded.
+ */
+ function onImagesLoaded(images, callback) {
+ var self = this,
+ unloaded = images.length;
+
+ if (unloaded === 0) {
+ callback();
+ }
+
+ images.each(function () {
+ if (this.complete) {
+ singleImageLoaded();
+ } else if (typeof this.naturalWidth !== 'undefined' && this.naturalWidth > 0) {
+ singleImageLoaded();
+ } else {
+ $(this).one('load', function () {
+ singleImageLoaded();
+ });
+ }
+ });
+
+ function singleImageLoaded() {
+ unloaded--;
+ if (unloaded === 0) {
+ callback();
+ }
+ }
+ }
+
+ Foundation.Timer = Timer;
+ Foundation.onImagesLoaded = onImagesLoaded;
+}(jQuery);
+//**************************************************
+//**Work inspired by multiple jquery swipe plugins**
+//**Done by Yohai Ararat ***************************
+//**************************************************
+(function ($) {
+
+ $.spotSwipe = {
+ version: '1.0.0',
+ enabled: 'ontouchstart' in document.documentElement,
+ preventDefault: false,
+ moveThreshold: 75,
+ timeThreshold: 200
+ };
+
+ var startPosX,
+ startPosY,
+ startTime,
+ elapsedTime,
+ isMoving = false;
+
+ function onTouchEnd() {
+ // alert(this);
+ this.removeEventListener('touchmove', onTouchMove);
+ this.removeEventListener('touchend', onTouchEnd);
+ isMoving = false;
+ }
+
+ function onTouchMove(e) {
+ if ($.spotSwipe.preventDefault) {
+ e.preventDefault();
+ }
+ if (isMoving) {
+ var x = e.touches[0].pageX;
+ var y = e.touches[0].pageY;
+ var dx = startPosX - x;
+ var dy = startPosY - y;
+ var dir;
+ elapsedTime = new Date().getTime() - startTime;
+ if (Math.abs(dx) >= $.spotSwipe.moveThreshold && elapsedTime <= $.spotSwipe.timeThreshold) {
+ dir = dx > 0 ? 'left' : 'right';
+ }
+ // else if(Math.abs(dy) >= $.spotSwipe.moveThreshold && elapsedTime <= $.spotSwipe.timeThreshold) {
+ // dir = dy > 0 ? 'down' : 'up';
+ // }
+ if (dir) {
+ e.preventDefault();
+ onTouchEnd.call(this);
+ $(this).trigger('swipe', dir).trigger('swipe' + dir);
+ }
+ }
+ }
+
+ function onTouchStart(e) {
+ if (e.touches.length == 1) {
+ startPosX = e.touches[0].pageX;
+ startPosY = e.touches[0].pageY;
+ isMoving = true;
+ startTime = new Date().getTime();
+ this.addEventListener('touchmove', onTouchMove, false);
+ this.addEventListener('touchend', onTouchEnd, false);
+ }
+ }
+
+ function init() {
+ this.addEventListener && this.addEventListener('touchstart', onTouchStart, false);
+ }
+
+ function teardown() {
+ this.removeEventListener('touchstart', onTouchStart);
+ }
+
+ $.event.special.swipe = { setup: init };
+
+ $.each(['left', 'up', 'down', 'right'], function () {
+ $.event.special['swipe' + this] = { setup: function () {
+ $(this).on('swipe', $.noop);
+ } };
+ });
+})(jQuery);
+/****************************************************
+ * Method for adding psuedo drag events to elements *
+ ***************************************************/
+!function ($) {
+ $.fn.addTouch = function () {
+ this.each(function (i, el) {
+ $(el).bind('touchstart touchmove touchend touchcancel', function () {
+ //we pass the original event object because the jQuery event
+ //object is normalized to w3c specs and does not provide the TouchList
+ handleTouch(event);
+ });
+ });
+
+ var handleTouch = function (event) {
+ var touches = event.changedTouches,
+ first = touches[0],
+ eventTypes = {
+ touchstart: 'mousedown',
+ touchmove: 'mousemove',
+ touchend: 'mouseup'
+ },
+ type = eventTypes[event.type],
+ simulatedEvent;
+
+ if ('MouseEvent' in window && typeof window.MouseEvent === 'function') {
+ simulatedEvent = new window.MouseEvent(type, {
+ 'bubbles': true,
+ 'cancelable': true,
+ 'screenX': first.screenX,
+ 'screenY': first.screenY,
+ 'clientX': first.clientX,
+ 'clientY': first.clientY
+ });
+ } else {
+ simulatedEvent = document.createEvent('MouseEvent');
+ simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0 /*left*/, null);
+ }
+ first.target.dispatchEvent(simulatedEvent);
+ };
+ };
+}(jQuery);
+
+//**********************************
+//**From the jQuery Mobile Library**
+//**need to recreate functionality**
+//**and try to improve if possible**
+//**********************************
+
+/* Removing the jQuery function ****
+************************************
+
+(function( $, window, undefined ) {
+
+ var $document = $( document ),
+ // supportTouch = $.mobile.support.touch,
+ touchStartEvent = 'touchstart'//supportTouch ? "touchstart" : "mousedown",
+ touchStopEvent = 'touchend'//supportTouch ? "touchend" : "mouseup",
+ touchMoveEvent = 'touchmove'//supportTouch ? "touchmove" : "mousemove";
+
+ // setup new event shortcuts
+ $.each( ( "touchstart touchmove touchend " +
+ "swipe swipeleft swiperight" ).split( " " ), function( i, name ) {
+
+ $.fn[ name ] = function( fn ) {
+ return fn ? this.bind( name, fn ) : this.trigger( name );
+ };
+
+ // jQuery < 1.8
+ if ( $.attrFn ) {
+ $.attrFn[ name ] = true;
+ }
+ });
+
+ function triggerCustomEvent( obj, eventType, event, bubble ) {
+ var originalType = event.type;
+ event.type = eventType;
+ if ( bubble ) {
+ $.event.trigger( event, undefined, obj );
+ } else {
+ $.event.dispatch.call( obj, event );
+ }
+ event.type = originalType;
+ }
+
+ // also handles taphold
+
+ // Also handles swipeleft, swiperight
+ $.event.special.swipe = {
+
+ // More than this horizontal displacement, and we will suppress scrolling.
+ scrollSupressionThreshold: 30,
+
+ // More time than this, and it isn't a swipe.
+ durationThreshold: 1000,
+
+ // Swipe horizontal displacement must be more than this.
+ horizontalDistanceThreshold: window.devicePixelRatio >= 2 ? 15 : 30,
+
+ // Swipe vertical displacement must be less than this.
+ verticalDistanceThreshold: window.devicePixelRatio >= 2 ? 15 : 30,
+
+ getLocation: function ( event ) {
+ var winPageX = window.pageXOffset,
+ winPageY = window.pageYOffset,
+ x = event.clientX,
+ y = event.clientY;
+
+ if ( event.pageY === 0 && Math.floor( y ) > Math.floor( event.pageY ) ||
+ event.pageX === 0 && Math.floor( x ) > Math.floor( event.pageX ) ) {
+
+ // iOS4 clientX/clientY have the value that should have been
+ // in pageX/pageY. While pageX/page/ have the value 0
+ x = x - winPageX;
+ y = y - winPageY;
+ } else if ( y < ( event.pageY - winPageY) || x < ( event.pageX - winPageX ) ) {
+
+ // Some Android browsers have totally bogus values for clientX/Y
+ // when scrolling/zooming a page. Detectable since clientX/clientY
+ // should never be smaller than pageX/pageY minus page scroll
+ x = event.pageX - winPageX;
+ y = event.pageY - winPageY;
+ }
+
+ return {
+ x: x,
+ y: y
+ };
+ },
+
+ start: function( event ) {
+ var data = event.originalEvent.touches ?
+ event.originalEvent.touches[ 0 ] : event,
+ location = $.event.special.swipe.getLocation( data );
+ return {
+ time: ( new Date() ).getTime(),
+ coords: [ location.x, location.y ],
+ origin: $( event.target )
+ };
+ },
+
+ stop: function( event ) {
+ var data = event.originalEvent.touches ?
+ event.originalEvent.touches[ 0 ] : event,
+ location = $.event.special.swipe.getLocation( data );
+ return {
+ time: ( new Date() ).getTime(),
+ coords: [ location.x, location.y ]
+ };
+ },
+
+ handleSwipe: function( start, stop, thisObject, origTarget ) {
+ if ( stop.time - start.time < $.event.special.swipe.durationThreshold &&
+ Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold &&
+ Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) {
+ var direction = start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight";
+
+ triggerCustomEvent( thisObject, "swipe", $.Event( "swipe", { target: origTarget, swipestart: start, swipestop: stop }), true );
+ triggerCustomEvent( thisObject, direction,$.Event( direction, { target: origTarget, swipestart: start, swipestop: stop } ), true );
+ return true;
+ }
+ return false;
+
+ },
+
+ // This serves as a flag to ensure that at most one swipe event event is
+ // in work at any given time
+ eventInProgress: false,
+
+ setup: function() {
+ var events,
+ thisObject = this,
+ $this = $( thisObject ),
+ context = {};
+
+ // Retrieve the events data for this element and add the swipe context
+ events = $.data( this, "mobile-events" );
+ if ( !events ) {
+ events = { length: 0 };
+ $.data( this, "mobile-events", events );
+ }
+ events.length++;
+ events.swipe = context;
+
+ context.start = function( event ) {
+
+ // Bail if we're already working on a swipe event
+ if ( $.event.special.swipe.eventInProgress ) {
+ return;
+ }
+ $.event.special.swipe.eventInProgress = true;
+
+ var stop,
+ start = $.event.special.swipe.start( event ),
+ origTarget = event.target,
+ emitted = false;
+
+ context.move = function( event ) {
+ if ( !start || event.isDefaultPrevented() ) {
+ return;
+ }
+
+ stop = $.event.special.swipe.stop( event );
+ if ( !emitted ) {
+ emitted = $.event.special.swipe.handleSwipe( start, stop, thisObject, origTarget );
+ if ( emitted ) {
+
+ // Reset the context to make way for the next swipe event
+ $.event.special.swipe.eventInProgress = false;
+ }
+ }
+ // prevent scrolling
+ if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) {
+ event.preventDefault();
+ }
+ };
+
+ context.stop = function() {
+ emitted = true;
+
+ // Reset the context to make way for the next swipe event
+ $.event.special.swipe.eventInProgress = false;
+ $document.off( touchMoveEvent, context.move );
+ context.move = null;
+ };
+
+ $document.on( touchMoveEvent, context.move )
+ .one( touchStopEvent, context.stop );
+ };
+ $this.on( touchStartEvent, context.start );
+ },
+
+ teardown: function() {
+ var events, context;
+
+ events = $.data( this, "mobile-events" );
+ if ( events ) {
+ context = events.swipe;
+ delete events.swipe;
+ events.length--;
+ if ( events.length === 0 ) {
+ $.removeData( this, "mobile-events" );
+ }
+ }
+
+ if ( context ) {
+ if ( context.start ) {
+ $( this ).off( touchStartEvent, context.start );
+ }
+ if ( context.move ) {
+ $document.off( touchMoveEvent, context.move );
+ }
+ if ( context.stop ) {
+ $document.off( touchStopEvent, context.stop );
+ }
+ }
+ }
+ };
+ $.each({
+ swipeleft: "swipe.left",
+ swiperight: "swipe.right"
+ }, function( event, sourceEvent ) {
+
+ $.event.special[ event ] = {
+ setup: function() {
+ $( this ).bind( sourceEvent, $.noop );
+ },
+ teardown: function() {
+ $( this ).unbind( sourceEvent );
+ }
+ };
+ });
+})( jQuery, this );
+*/
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+!function ($) {
+
+ /**
+ * Abide module.
+ * @module foundation.abide
+ */
+
+ var Abide = function () {
+ /**
+ * Creates a new instance of Abide.
+ * @class
+ * @fires Abide#init
+ * @param {Object} element - jQuery object to add the trigger to.
+ * @param {Object} options - Overrides to the default plugin settings.
+ */
+
+ function Abide(element) {
+ var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, Abide);
+
+ this.$element = element;
+ this.options = $.extend({}, Abide.defaults, this.$element.data(), options);
+
+ this._init();
+
+ Foundation.registerPlugin(this, 'Abide');
+ }
+
+ /**
+ * Initializes the Abide plugin and calls functions to get Abide functioning on load.
+ * @private
+ */
+
+
+ _createClass(Abide, [{
+ key: '_init',
+ value: function _init() {
+ this.$inputs = this.$element.find('input, textarea, select');
+
+ this._events();
+ }
+
+ /**
+ * Initializes events for Abide.
+ * @private
+ */
+
+ }, {
+ key: '_events',
+ value: function _events() {
+ var _this2 = this;
+
+ this.$element.off('.abide').on('reset.zf.abide', function () {
+ _this2.resetForm();
+ }).on('submit.zf.abide', function () {
+ return _this2.validateForm();
+ });
+
+ if (this.options.validateOn === 'fieldChange') {
+ this.$inputs.off('change.zf.abide').on('change.zf.abide', function (e) {
+ _this2.validateInput($(e.target));
+ });
+ }
+
+ if (this.options.liveValidate) {
+ this.$inputs.off('input.zf.abide').on('input.zf.abide', function (e) {
+ _this2.validateInput($(e.target));
+ });
+ }
+ }
+
+ /**
+ * Calls necessary functions to update Abide upon DOM change
+ * @private
+ */
+
+ }, {
+ key: '_reflow',
+ value: function _reflow() {
+ this._init();
+ }
+
+ /**
+ * Checks whether or not a form element has the required attribute and if it's checked or not
+ * @param {Object} element - jQuery object to check for required attribute
+ * @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
+ */
+
+ }, {
+ key: 'requiredCheck',
+ value: function requiredCheck($el) {
+ if (!$el.attr('required')) return true;
+
+ var isGood = true;
+
+ switch ($el[0].type) {
+ case 'checkbox':
+ isGood = $el[0].checked;
+ break;
+
+ case 'select':
+ case 'select-one':
+ case 'select-multiple':
+ var opt = $el.find('option:selected');
+ if (!opt.length || !opt.val()) isGood = false;
+ break;
+
+ default:
+ if (!$el.val() || !$el.val().length) isGood = false;
+ }
+
+ return isGood;
+ }
+
+ /**
+ * Based on $el, get the first element with selector in this order:
+ * 1. The element's direct sibling('s).
+ * 3. The element's parent's children.
+ *
+ * This allows for multiple form errors per input, though if none are found, no form errors will be shown.
+ *
+ * @param {Object} $el - jQuery object to use as reference to find the form error selector.
+ * @returns {Object} jQuery object with the selector.
+ */
+
+ }, {
+ key: 'findFormError',
+ value: function findFormError($el) {
+ var $error = $el.siblings(this.options.formErrorSelector);
+
+ if (!$error.length) {
+ $error = $el.parent().find(this.options.formErrorSelector);
+ }
+
+ return $error;
+ }
+
+ /**
+ * Get the first element in this order:
+ * 2. The
+
+<%end%>
+
diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb
new file mode 100644
index 0000000000..b0c24a3a98
--- /dev/null
+++ b/app/views/categories/new.html.erb
@@ -0,0 +1,42 @@
+Create a category:
+Categories must have unique names and cannot be deleted.
+
+<% if flash[:error] %>
+ <%= flash[:error] %>
+<% end %>
+
+
+ <% if @category.errors.any? %>
+
+ <% @category.errors.each do |column, message| %>
+ -
+
+ <%= column.capitalize %>
+ <%= message %>
+
+ <% end %>
+
+ <% end %>
+
+ <%= form_for @category, url: categories_path do |f| %>
+
+ <%= f.label :name %>
+ <%= f.text_field :name %>
+
+
+
+ <%= f.submit "Submit" %>
+
+ <% end %>
+
+
+Here's a list of current categories...
+
+<% @categories.each do |category| %>
+ -
+
+
<%= category.name %>
+
+
+<%end%>
+
diff --git a/app/views/categories/show.html.erb b/app/views/categories/show.html.erb
new file mode 100644
index 0000000000..506306668e
--- /dev/null
+++ b/app/views/categories/show.html.erb
@@ -0,0 +1,18 @@
+Category: <%= capitalize_each_word(@category.name) %>
+
+<% if @products.empty? %>
+
+ There are not any products currently in this category.
+
+<% end %>
+
+
+<% @products.each do |product| %>
+ -
+
+ <%= link_to (image_tag "#{ product.photo_url }", alt: "#{ product.name }", class: "product-image"), product_path(product) %>
+
<%= link_to capitalize_each_word(product.name), product_path(product) %>
+
+
+<%end%>
+
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb
new file mode 100644
index 0000000000..74c41c3352
--- /dev/null
+++ b/app/views/home/index.html.erb
@@ -0,0 +1,19 @@
+
+<% if flash[:error] %>
+ <%= flash[:error] %>
+<% end %>
+
+~Top Products~
+
+
+<% @products.each do |product| %>
+ -
+
+ <%= link_to (image_tag "#{ product.photo_url }", alt: "#{ product.name }", class: "product-image"), product_path(product) %>
+
+
+ <%= link_to capitalize_each_word(product.name), product_path(product) %>
+
+
+<%end%>
+
diff --git a/app/views/layouts/_cart_text.html.erb b/app/views/layouts/_cart_text.html.erb
new file mode 100644
index 0000000000..89d05ac0f6
--- /dev/null
+++ b/app/views/layouts/_cart_text.html.erb
@@ -0,0 +1 @@
+<%= link_to "#{current_order.order_items.size} Items in Cart ( #{number_to_currency current_order.total} )", cart_path, class: "btn btn-link" %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 9b70e01c2e..ea0b253f6c 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,14 +1,38 @@
- Betsy
+ Foot Foot Couture
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
+
+
+
+ ~FootFoot Couture~
-<%= yield %>
+
+
+<%= yield %>
+
+
diff --git a/app/views/merchants/index.html.erb b/app/views/merchants/index.html.erb
new file mode 100644
index 0000000000..07012f8ee8
--- /dev/null
+++ b/app/views/merchants/index.html.erb
@@ -0,0 +1,11 @@
+All Merchants
+
+
+<% @merchants.each do |merchant| %>
+ -
+
+
<%= link_to capitalize_each_word(merchant.user_name), merchant_path(merchant) %>
+
+
+<%end%>
+
diff --git a/app/views/merchants/show.html.erb b/app/views/merchants/show.html.erb
new file mode 100644
index 0000000000..4cf39d7d79
--- /dev/null
+++ b/app/views/merchants/show.html.erb
@@ -0,0 +1,18 @@
+<%= @merchant.user_name %>'s Products
+
+<% if @products.empty? %>
+
+ Currently, this merchant does not have any products.
+
+<% end %>
+
+
+<% @products.each do |product| %>
+ -
+
+ <%= link_to (image_tag "#{ product.photo_url }", alt: "#{ product.name }", class: "product-image"), product_path(product) %>
+
<%= link_to capitalize_each_word(product.name), product_path(product) %>
+
+
+<%end%>
+
diff --git a/app/views/order_items/_form.html.erb b/app/views/order_items/_form.html.erb
new file mode 100644
index 0000000000..41eaa1a8df
--- /dev/null
+++ b/app/views/order_items/_form.html.erb
@@ -0,0 +1,5 @@
+<%= form_for @order_item do |f| %>
+<%= f.label :quantity %>
+<%= f.text_field :quantity %>
+<%= f.submit %>
+<% end %>
diff --git a/app/views/order_items/edit.html.erb b/app/views/order_items/edit.html.erb
new file mode 100644
index 0000000000..d44c608b6a
--- /dev/null
+++ b/app/views/order_items/edit.html.erb
@@ -0,0 +1 @@
+<%= render partial: "form" %>
diff --git a/app/views/orders/create.html.erb b/app/views/orders/create.html.erb
new file mode 100644
index 0000000000..295bd84094
--- /dev/null
+++ b/app/views/orders/create.html.erb
@@ -0,0 +1,2 @@
+Orders#create
+Find me in app/views/orders/create.html.erb
diff --git a/app/views/orders/destroy.html.erb b/app/views/orders/destroy.html.erb
new file mode 100644
index 0000000000..d14d0a3508
--- /dev/null
+++ b/app/views/orders/destroy.html.erb
@@ -0,0 +1,2 @@
+Orders#destroy
+Find me in app/views/orders/destroy.html.erb
diff --git a/app/views/orders/index.html.erb b/app/views/orders/index.html.erb
new file mode 100644
index 0000000000..a758adace1
--- /dev/null
+++ b/app/views/orders/index.html.erb
@@ -0,0 +1,4 @@
+
+ Thank you for your order <%="#{@order["cc_name"]}"%>!
+ Your order number is: <%= "#{@order["id"]}" %>
+ Your total comes to: <%= show_dollars(@order.total)%>
diff --git a/app/views/orders/manage.html.erb b/app/views/orders/manage.html.erb
new file mode 100644
index 0000000000..d9a6cfb4f8
--- /dev/null
+++ b/app/views/orders/manage.html.erb
@@ -0,0 +1,16 @@
+ Manage Orders
+
+<% @orders.each do |order| %>
+
+ Status: <%= order.status %>
+
+
+ Email: <%= order.email %>
+
+
+ Date Purchased: <%= date_format(order.date_purchased) %>
+
+
+
+
+<% end %>
diff --git a/app/views/orders/new.html.erb b/app/views/orders/new.html.erb
new file mode 100644
index 0000000000..a19fe23635
--- /dev/null
+++ b/app/views/orders/new.html.erb
@@ -0,0 +1,19 @@
+Checkout
+<%= form_for @order do |f| %>
+<%= f.label :email%>
+<%= f.text_field :email %>
+<%= f.label :address %>
+<%= f.text_field :address %>
+<%= f.label "Name on the Credit Card" %>
+<%= f.text_field :cc_name %>
+<%= f.label "Credit Card Number" %>
+<%= f.text_field :cc_number %>
+<%= f.label "Expiration Year" %>
+<%= f.select :cc_exp_year, options_for_select(years) %>
+<%= f.label "Expiration Month" %>
+<%= f.select :cc_exp_month, options_for_select(months) %>
+<%= f.label "Zip Code" %>
+<%= f.text_field :billing_zip %>
+<%= f.hidden_field :date_purchased, :value => Time.now %>
+<%= f.submit%>
+<% end %>
diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb
new file mode 100644
index 0000000000..22eb495f6f
--- /dev/null
+++ b/app/views/orders/show.html.erb
@@ -0,0 +1,2 @@
+Orders#show
+Find me in app/views/orders/show.html.erb
diff --git a/app/views/orders/update.html.erb b/app/views/orders/update.html.erb
new file mode 100644
index 0000000000..21caac1f70
--- /dev/null
+++ b/app/views/orders/update.html.erb
@@ -0,0 +1,2 @@
+Orders#update
+Find me in app/views/orders/update.html.erb
diff --git a/app/views/products/_form.html.erb b/app/views/products/_form.html.erb
new file mode 100644
index 0000000000..15ff721480
--- /dev/null
+++ b/app/views/products/_form.html.erb
@@ -0,0 +1,56 @@
+
+ <% if @product.errors.any? %>
+
+ <% @product.errors.each do |column, message| %>
+ -
+
+ <%= column.capitalize %>
+ <%= message %>
+
+ <% end %>
+
+ <% end %>
+
+ <%= form_for @product, url: local_path do |f| %>
+
+ <%= f.label :name %>
+ <%= f.text_field :name %>
+
+
+
+ <%= f.label :description %>
+ <%= f.text_field :description %>
+
+
+
+ <%= f.label :stock, "How many are you selling? (max 500)" %>
+ <%= f.number_field :stock, in: 1..500 %>
+
+
+
+ <%= f.label :price, "Price (in cents)" %>
+ <%= f.text_field :price %>
+
+
+
+ <%= f.label :photo_url, "URL of photo (sorry, we can't host photos)" %>
+ <%= f.url_field :photo_url %>
+
+
+
+ <%= f.label "Choose at least one category" %>
+
+ <%= f.collection_check_boxes :category_ids, Category.all, :id, :name do |b| %>
+ -
+ <%= b.check_box %>
+ <%= b.label %>
+
+ <% end %>
+
+
+
+
+ <%= f.submit "Submit" %>
+
+ <% end %>
+
diff --git a/app/views/products/_product_row.html.erb b/app/views/products/_product_row.html.erb
new file mode 100644
index 0000000000..dd73d4f7b2
--- /dev/null
+++ b/app/views/products/_product_row.html.erb
@@ -0,0 +1,22 @@
+
+
+
+
+
<%= product.name %>
+
+
+
+ <%= form_for @order_item, remote: true do |f| %>
+
Unit Price: <%= number_to_currency product.price %>
+
+ <% end %>
+
+
+
+
diff --git a/app/views/products/edit.html.erb b/app/views/products/edit.html.erb
new file mode 100644
index 0000000000..86b3669939
--- /dev/null
+++ b/app/views/products/edit.html.erb
@@ -0,0 +1,5 @@
+Edit <%= @product.name %>
+
+<%= render partial: "form",
+ locals: { http_method: :patch,
+ local_path: product_path(@product) } %>
diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb
new file mode 100644
index 0000000000..445ebeba4e
--- /dev/null
+++ b/app/views/products/index.html.erb
@@ -0,0 +1,12 @@
+All Products
+
+
+<% @products.each do |product| %>
+ -
+
+ <%= link_to (image_tag "#{ product.photo_url }", alt: "#{ product.name }", class: "product-image"), product_path(product) %>
+
<%= link_to capitalize_each_word(product.name), product_path(product) %>
+
+
+<%end%>
+
diff --git a/app/views/products/manage.html.erb b/app/views/products/manage.html.erb
new file mode 100644
index 0000000000..fb555ed122
--- /dev/null
+++ b/app/views/products/manage.html.erb
@@ -0,0 +1,22 @@
+Product management:
+
+<% if flash[:error] %>
+ <%= flash[:error] %>
+<% end %>
+
+
+<% @products.each do |product| %>
+ -
+
+
<%= capitalize_each_word(product.name) %>
+ <%= link_to "EDIT", edit_product_path(product), class: "button" %>
+ <% if product.active %>
+ <%= link_to "RETIRE", retire_product_path(product), remote: true, method: "patch", class: "warning button" %>
+ <% else %>
+ <%= link_to "ACTIVATE", retire_product_path(product), remote: true, method: "patch", class: "warning button" %>
+ <% end %>
+ <%= link_to "DELETE", product_path(product), remote: true, method: "delete", class: "alert button" %>
+
+
+<%end%>
+
diff --git a/app/views/products/new.html.erb b/app/views/products/new.html.erb
new file mode 100644
index 0000000000..3ef63c2f9b
--- /dev/null
+++ b/app/views/products/new.html.erb
@@ -0,0 +1,5 @@
+Add a New Product
+
+<%= render partial: "form",
+ locals: { http_method: :post,
+ local_path: products_path } %>
diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb
new file mode 100644
index 0000000000..21bfea7dea
--- /dev/null
+++ b/app/views/products/show.html.erb
@@ -0,0 +1,73 @@
+<% if flash[:error] %>
+ <%= flash[:error] %>
+<% end %>
+
+
+
+ <%= capitalize_each_word(@product.name) %>
+
+ <%= image_tag "#{@product.photo_url}", alt: "#{@product.name}", class: "product-image" %>
+
+
+
+
+
+ Price: <%= show_dollars(@product.price) %>
+
+
+
+ In Stock: <%= @product.stock %>
+
+
+
+ Description:
+
+ <%= @product.description %>
+
+
+
+ <% unless @product.categories.empty? %>
+
+ - Categories:
+ <% @product.categories.each do |category| %>
+ -
+ <%= link_to "#{category.name}", category_path(category), class: "category-link" %>
+
+ <% end %>
+
+ <% end %>
+ <%= link_to "Add to cart", add_cart_path(@product), class: "hollow button" %>
+
+
+
+<%# REVIEWS SECTION: %>
+
+
+ Reviews
+ <% if @product.reviews.empty? %>
+
+ No reviews! <%= link_to "Be the first to review!", new_product_review_path(@product.id) %>
+
+ <% else %>
+ <% @product.reviews.each do |review| %>
+
+
+
+ <%= date_format(review.created_at) %>
+
+
+
+ Rating: <%= review.rating %>
+
+
+
+ Description: <%= review.description %>
+
+ <% end %>
+
+
+ <%= link_to "Review this product!", new_product_review_path(@product.id) %>
+
+ <% end %>
+
+
diff --git a/app/views/reviews/_form.html.erb b/app/views/reviews/_form.html.erb
new file mode 100644
index 0000000000..4eb0e2050b
--- /dev/null
+++ b/app/views/reviews/_form.html.erb
@@ -0,0 +1,32 @@
+
+ <%= form_for @review, method: http_method, url: local_path do |f| %>
+ <% if @review.errors.any? %>
+
+
+ <% @review.errors.each do |column, message| %>
+ -
+ <%= column.capitalize %>
+ <%= message %>
+
+ <% end %>
+
+
+ <% end %>
+
+
+
+ <%= f.label :rating, "Rating (1 (worst) to 5 (best)): " %>
+ <%= f.number_field :rating, in: 1..5 %>
+
+
+
+ <%= f.label :description, "Description (max 400 characters):" %>
+
+
+ <%= f.text_area :description, size: "35x10" %>
+
+
+ <%= f.submit "Submit", class: "success button" %>
+ <% end %>
+
+
diff --git a/app/views/reviews/new.html.erb b/app/views/reviews/new.html.erb
new file mode 100644
index 0000000000..3a237816c5
--- /dev/null
+++ b/app/views/reviews/new.html.erb
@@ -0,0 +1,12 @@
+Review <%= capitalize_each_word(@product.name) %>
+<%#= image_tag "#{@product.photo_url}", alt: "#{@product.name}" %>
+
+<%= render partial: "form",
+ locals: {
+ http_method: :post,
+ local_path: product_reviews_path
+ } %>
+
+
+ <%= link_to "Back to Product", product_path(@product) %>
+
diff --git a/app/views/sessions/index.html.erb b/app/views/sessions/index.html.erb
new file mode 100644
index 0000000000..a483fd1ad5
--- /dev/null
+++ b/app/views/sessions/index.html.erb
@@ -0,0 +1,24 @@
+Merchant Portal: Welcome <%= @merchant.email %>!
+
+
+ -
+
+
<%= link_to "Manage your products", manage_products_path %>
+
+
+ -
+
+
<%= link_to "Add a new product", new_product_path %>
+
+
+ -
+
+
<%= link_to "Create a new category", new_category_path %>
+
+
+ -
+
+
<%= link_to "Manage your orders", manage_orders_path %>
+
+
+
diff --git a/app/views/sessions/login.html.erb b/app/views/sessions/login.html.erb
new file mode 100644
index 0000000000..095b127bff
--- /dev/null
+++ b/app/views/sessions/login.html.erb
@@ -0,0 +1 @@
+You are not logged in. Please log in using the above navigation link to the right - or access the top left options without logging in.
diff --git a/app/views/sessions/login_failure.html.erb b/app/views/sessions/login_failure.html.erb
new file mode 100644
index 0000000000..fbcf1eec0a
--- /dev/null
+++ b/app/views/sessions/login_failure.html.erb
@@ -0,0 +1,3 @@
+You failed to login.
+
+Please log into your account
diff --git a/bin/spring b/bin/spring
index 7fe232c3aa..9bc076b9ea 100755
--- a/bin/spring
+++ b/bin/spring
@@ -7,9 +7,10 @@ unless defined?(Spring)
require 'rubygems'
require 'bundler'
- if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
- Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
- gem 'spring', match[1]
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
+ if spring = lockfile.specs.detect { |spec| spec.name == "spring" }
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
+ gem 'spring', spring.version
require 'spring/binstub'
end
end
diff --git a/config/database.yml b/config/database.yml
index 04f24be2ba..1c1a37ca8d 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -1,85 +1,25 @@
-# PostgreSQL. Versions 8.2 and up are supported.
+# SQLite version 3.x
+# gem install sqlite3
#
-# Install the pg driver:
-# gem install pg
-# On OS X with Homebrew:
-# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
-# On OS X with MacPorts:
-# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
-# On Windows:
-# gem install pg
-# Choose the win32 build.
-# Install PostgreSQL and put its /bin directory on your path.
-#
-# Configure Using Gemfile
-# gem 'pg'
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
#
default: &default
- adapter: postgresql
- encoding: unicode
- # For details on connection pooling, see rails configuration guide
- # http://guides.rubyonrails.org/configuring.html#database-pooling
+ adapter: sqlite3
pool: 5
+ timeout: 5000
development:
<<: *default
- database: betsy_development
-
- # The specified database role being used to connect to postgres.
- # To create additional roles in postgres see `$ createuser --help`.
- # When left blank, postgres will use the default role. This is
- # the same name as the operating system user that initialized the database.
- #username: betsy
-
- # The password associated with the postgres role (username).
- #password:
-
- # Connect on a TCP socket. Omitted by default since the client uses a
- # domain socket that doesn't need configuration. Windows does not have
- # domain sockets, so uncomment these lines.
- #host: localhost
-
- # The TCP port the server listens on. Defaults to 5432.
- # If your server runs on a different port number, change accordingly.
- #port: 5432
-
- # Schema search path. The server defaults to $user,public
- #schema_search_path: myapp,sharedapp,public
-
- # Minimum log levels, in increasing order:
- # debug5, debug4, debug3, debug2, debug1,
- # log, notice, warning, error, fatal, and panic
- # Defaults to warning.
- #min_messages: notice
+ database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
- database: betsy_test
+ database: db/test.sqlite3
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%= ENV['DATABASE_URL'] %>
-#
production:
<<: *default
- database: betsy_production
- username: betsy
- password: <%= ENV['BETSY_DATABASE_PASSWORD'] %>
+ database: db/production.sqlite3
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
new file mode 100644
index 0000000000..fd4416122a
--- /dev/null
+++ b/config/initializers/omniauth.rb
@@ -0,0 +1,3 @@
+Rails.application.config.middleware.use OmniAuth::Builder do
+ provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email"
+end
diff --git a/config/routes.rb b/config/routes.rb
index 3f66539d54..320caceff9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,56 +1,49 @@
Rails.application.routes.draw do
- # The priority is based upon order of creation: first created -> highest priority.
- # See how all your routes lay out with "rake routes".
-
- # You can have the root of your site routed with "root"
- # root 'welcome#index'
-
- # Example of regular route:
- # get 'products/:id' => 'catalog#view'
-
- # Example of named route that can be invoked with purchase_url(id: product.id)
- # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
-
- # Example resource route (maps HTTP verbs to controller actions automatically):
- # resources :products
-
- # Example resource route with options:
- # resources :products do
- # member do
- # get 'short'
- # post 'toggle'
- # end
- #
- # collection do
- # get 'sold'
- # end
- # end
-
- # Example resource route with sub-resources:
- # resources :products do
- # resources :comments, :sales
- # resource :seller
- # end
-
- # Example resource route with more complex sub-resources:
- # resources :products do
- # resources :comments
- # resources :sales do
- # get 'recent', on: :collection
- # end
- # end
-
- # Example resource route with concerns:
- # concern :toggleable do
- # post 'toggle'
- # end
- # resources :posts, concerns: :toggleable
- # resources :photos, concerns: :toggleable
-
- # Example resource route within a namespace:
- # namespace :admin do
- # # Directs /admin/products/* to Admin::ProductsController
- # # (app/controllers/admin/products_controller.rb)
- # resources :products
- # end
+
+ root 'home#index'
+
+ resources :merchants, only: [:index, :show]
+
+ # We want logged in merchants to be able to view ALL products in an index--and to manage only their products, so I'm adding in a products#manage Controller action to create this:
+
+
+ get '/products/manage', to: 'products#manage', as: 'manage_products'
+ get '/orders/manage', to: 'orders#manage', as: 'manage_orders'
+
+
+ resources :products do
+ resources :reviews, only: [:new, :create]
+ end
+
+ patch 'products/:id/retire' => 'products#retire', as: :retire_product
+
+ resources :orders
+
+ resources :order_items, except: [:index, :show]
+ resources :categories, only: [:index, :new, :create, :show]
+
+
+ # Sessions routes
+
+ # We're going to talk about this more if any of us needs to edit this. :)
+ # resources :orders, only: [:new, :create, :show] do
+ # # resources :order_items, except: [:index, :show]
+ # end
+
+ # Sessions routes - can be further flushed out...
+
+ get '/auth/:provider/callback' => 'sessions#create'
+ get "/sessions/login_failure", to: "sessions#login_failure", as: "login_failure"
+ get '/sessions', to: 'sessions#index', as: 'portal'
+ delete '/auth/logout', to: 'sessions#logout', as: "logout"
+ get '/auth/login', to: 'sessions#login', as: 'login'
+
+ #specific routes for the cart!
+ get '/carts' => 'carts#index'
+ get 'carts/empty_cart' =>'carts#empty_cart'
+ get '/carts/:id', to: 'carts#add_to_cart', as: "add_cart"
+ patch 'carts/:id', to: 'carts#more_prod', as: 'more_products'
+ patch 'carts/:id/reduce', to: 'carts#less_prod', as: 'less_products'
+ delete '/carts/:id/delete_product', to: 'carts#delete_product', as: 'delete_products'
+ delete '/carts/products/:id', to: 'carts#destroy', as: 'delete_cart'
end
diff --git a/config/secrets.yml b/config/secrets.yml
index 6442868fc1..d26d6727cf 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -11,10 +11,10 @@
# if you're sharing your code publicly.
development:
- secret_key_base: eaebee6b0ce07b1e5b22743dacf6e6aaec01bf01c88370f9f86c3ae0d8592b4313c5e9da09754cd613eda8d206168bd74d5ed9910230b3957725352c70e2cf80
+ secret_key_base: 6ecbe27cf12a699c0f0842fcb1ce4add1a562a92f96d08f03fa31192a94cde8c5fa571fc9fdc4e96ad64b9e3aa11134cd85f4c7b0065a1ecbf26d3de3f8acb62
test:
- secret_key_base: f9c5bd6c383573755e5f8cf214402a51eb5ce8c7d0df9860dbf980f03fbd6c508e97412644b6a80d7034774602b05e05988bf7ab8ff594912950fdb590589a4d
+ secret_key_base: 81722cec9809519a189142b7cc0ab478412efc5da7e93a2490c862734470561356f2d1dfe64a2934052e123f02bf8d8954875d87d2bdf3a677efadf855489423
# Do not keep production secrets in the repository,
# instead read values from the environment.
diff --git a/db/migrate/20161018233022_create_reviews.rb b/db/migrate/20161018233022_create_reviews.rb
new file mode 100644
index 0000000000..b395d0d607
--- /dev/null
+++ b/db/migrate/20161018233022_create_reviews.rb
@@ -0,0 +1,18 @@
+class CreateReviews < ActiveRecord::Migration
+ def change
+ create_table :reviews do |t|
+ t.integer :rating
+ t.string :description
+ t.integer :product_id
+
+ t.timestamps null: false
+ end
+ end
+end
+
+
+# Reviews:
+# Rating: integer default to 1
+# Description: string
+# Product_ID
+# (belongs to a product)
diff --git a/db/migrate/20161019005406_create_merchants.rb b/db/migrate/20161019005406_create_merchants.rb
new file mode 100644
index 0000000000..758eccaa6f
--- /dev/null
+++ b/db/migrate/20161019005406_create_merchants.rb
@@ -0,0 +1,11 @@
+class CreateMerchants < ActiveRecord::Migration
+ def change
+ create_table :merchants do |t|
+ t.string :user_name
+ t.string :email
+ t.integer :uid, null: false
+ t.string :provider, null: false
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20161019043527_create_order_items.rb b/db/migrate/20161019043527_create_order_items.rb
new file mode 100644
index 0000000000..c3be31db3b
--- /dev/null
+++ b/db/migrate/20161019043527_create_order_items.rb
@@ -0,0 +1,12 @@
+class CreateOrderItems < ActiveRecord::Migration
+ def change
+ create_table :order_items do |t|
+ t.integer :quantity, :default => 1
+ t.integer :product_id
+ t.integer :order_id
+ t.boolean :shipped?, :default => false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20161019045258_create_products.rb b/db/migrate/20161019045258_create_products.rb
new file mode 100644
index 0000000000..d3eab0c07e
--- /dev/null
+++ b/db/migrate/20161019045258_create_products.rb
@@ -0,0 +1,14 @@
+class CreateProducts < ActiveRecord::Migration
+ def change
+ create_table :products do |t|
+ t.string :name
+ t.string :description
+ t.integer :stock
+ t.integer :price
+ t.string :photo_url
+ t.belongs_to :merchant, index: true
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20161019080932_create_categories.rb b/db/migrate/20161019080932_create_categories.rb
new file mode 100644
index 0000000000..4b0ff05055
--- /dev/null
+++ b/db/migrate/20161019080932_create_categories.rb
@@ -0,0 +1,8 @@
+class CreateCategories < ActiveRecord::Migration
+ def change
+ create_table :categories do |t|
+ t.string :name
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20161019094620_create_orders.rb b/db/migrate/20161019094620_create_orders.rb
new file mode 100644
index 0000000000..569c40288e
--- /dev/null
+++ b/db/migrate/20161019094620_create_orders.rb
@@ -0,0 +1,19 @@
+class CreateOrders < ActiveRecord::Migration
+ def change
+ create_table :orders do |t|
+
+ t.string :status
+ t.datetime :date_purchased
+ t.string :email
+ t.string :address
+ t.string :cc_name
+ t.integer :cc_number
+ t.integer :cc_exp_year
+ t.integer :cc_exp_month
+ t.integer :billing_zip
+ t.integer :total
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20161019205504_remove_total_from_order.rb b/db/migrate/20161019205504_remove_total_from_order.rb
new file mode 100644
index 0000000000..3f023ddf11
--- /dev/null
+++ b/db/migrate/20161019205504_remove_total_from_order.rb
@@ -0,0 +1,5 @@
+class RemoveTotalFromOrder < ActiveRecord::Migration
+ def change
+ remove_column(:orders, :total)
+ end
+end
diff --git a/db/migrate/20161020020026_create_join_table.rb b/db/migrate/20161020020026_create_join_table.rb
new file mode 100644
index 0000000000..f0eb23754e
--- /dev/null
+++ b/db/migrate/20161020020026_create_join_table.rb
@@ -0,0 +1,8 @@
+class CreateJoinTable < ActiveRecord::Migration
+ def change
+ create_join_table :products, :categories, id:false do |t|
+ t.index [:product_id, :category_id]
+ t.index [:category_id, :product_id]
+ end
+ end
+end
diff --git a/db/migrate/20161020212452_add_active_column_into_products_table.rb b/db/migrate/20161020212452_add_active_column_into_products_table.rb
new file mode 100644
index 0000000000..45ac8c9bfa
--- /dev/null
+++ b/db/migrate/20161020212452_add_active_column_into_products_table.rb
@@ -0,0 +1,5 @@
+class AddActiveColumnIntoProductsTable < ActiveRecord::Migration
+ def change
+ add_column(:products, :active?, :boolean, :default => true)
+ end
+end
diff --git a/db/migrate/20161020222236_oops_adding_total_back_into_order_table_yeni_is_wise.rb b/db/migrate/20161020222236_oops_adding_total_back_into_order_table_yeni_is_wise.rb
new file mode 100644
index 0000000000..017e951f4f
--- /dev/null
+++ b/db/migrate/20161020222236_oops_adding_total_back_into_order_table_yeni_is_wise.rb
@@ -0,0 +1,5 @@
+class OopsAddingTotalBackIntoOrderTableYeniIsWise < ActiveRecord::Migration
+ def change
+ add_column(:orders, :total, :integer, :default => 0)
+ end
+end
diff --git a/db/migrate/20161020231651_add_unit_price_to_order_items_table.rb b/db/migrate/20161020231651_add_unit_price_to_order_items_table.rb
new file mode 100644
index 0000000000..d4cb319224
--- /dev/null
+++ b/db/migrate/20161020231651_add_unit_price_to_order_items_table.rb
@@ -0,0 +1,6 @@
+class AddUnitPriceToOrderItemsTable < ActiveRecord::Migration
+ def change
+ add_column(:order_items, :unit_price, :integer)
+ add_column(:order_items, :total, :integer)
+ end
+end
diff --git a/db/migrate/20161021032202_rename_total_column_in_order_items_table.rb b/db/migrate/20161021032202_rename_total_column_in_order_items_table.rb
new file mode 100644
index 0000000000..ed5397c41d
--- /dev/null
+++ b/db/migrate/20161021032202_rename_total_column_in_order_items_table.rb
@@ -0,0 +1,5 @@
+class RenameTotalColumnInOrderItemsTable < ActiveRecord::Migration
+ def change
+ rename_column(:order_items, :total, :total_price)
+ end
+end
diff --git a/db/migrate/20161021201059_refactoring_with_shari.rb b/db/migrate/20161021201059_refactoring_with_shari.rb
new file mode 100644
index 0000000000..2acd10f98e
--- /dev/null
+++ b/db/migrate/20161021201059_refactoring_with_shari.rb
@@ -0,0 +1,6 @@
+class RefactoringWithShari < ActiveRecord::Migration
+ def change
+ remove_column(:order_items, :unit_price)
+ remove_column(:order_items, :total_price)
+ end
+end
diff --git a/db/migrate/20161024184627_orders_table_add_default_to_status.rb b/db/migrate/20161024184627_orders_table_add_default_to_status.rb
new file mode 100644
index 0000000000..18fd76f605
--- /dev/null
+++ b/db/migrate/20161024184627_orders_table_add_default_to_status.rb
@@ -0,0 +1,5 @@
+class OrdersTableAddDefaultToStatus < ActiveRecord::Migration
+ def change
+ change_column(:orders, :status, :string, :default => "PENDING")
+ end
+end
diff --git a/db/migrate/20161024221053_add_uniqueness_to_cat_prod.rb b/db/migrate/20161024221053_add_uniqueness_to_cat_prod.rb
new file mode 100644
index 0000000000..99d47f6230
--- /dev/null
+++ b/db/migrate/20161024221053_add_uniqueness_to_cat_prod.rb
@@ -0,0 +1,8 @@
+class AddUniquenessToCatProd < ActiveRecord::Migration
+ def change
+ remove_index(:categories_products, [:product_id, :category_id])
+ remove_index(:categories_products, [:category_id, :product_id])
+ add_index(:categories_products, [:product_id, :category_id], :unique => true)
+ add_index(:categories_products, [:category_id, :product_id], :unique => true)
+ end
+end
diff --git a/db/migrate/20161027220154_rename_active_column_in_products.rb b/db/migrate/20161027220154_rename_active_column_in_products.rb
new file mode 100644
index 0000000000..c083fdd8e6
--- /dev/null
+++ b/db/migrate/20161027220154_rename_active_column_in_products.rb
@@ -0,0 +1,5 @@
+class RenameActiveColumnInProducts < ActiveRecord::Migration
+ def change
+ rename_column(:products, :active?, :active)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000000..a75ce06c2a
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,85 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20161027220154) do
+
+ create_table "categories", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "categories_products", id: false, force: :cascade do |t|
+ t.integer "product_id", null: false
+ t.integer "category_id", null: false
+ end
+
+ add_index "categories_products", ["category_id", "product_id"], name: "index_categories_products_on_category_id_and_product_id", unique: true
+ add_index "categories_products", ["product_id", "category_id"], name: "index_categories_products_on_product_id_and_category_id", unique: true
+
+ create_table "merchants", force: :cascade do |t|
+ t.string "user_name"
+ t.string "email"
+ t.integer "uid", null: false
+ t.string "provider", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "order_items", force: :cascade do |t|
+ t.integer "quantity", default: 1
+ t.integer "product_id"
+ t.integer "order_id"
+ t.boolean "shipped?", default: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "orders", force: :cascade do |t|
+ t.string "status", default: "PENDING"
+ t.datetime "date_purchased"
+ t.string "email"
+ t.string "address"
+ t.string "cc_name"
+ t.integer "cc_number"
+ t.integer "cc_exp_year"
+ t.integer "cc_exp_month"
+ t.integer "billing_zip"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "total", default: 0
+ end
+
+ create_table "products", force: :cascade do |t|
+ t.string "name"
+ t.string "description"
+ t.integer "stock"
+ t.integer "price"
+ t.string "photo_url"
+ t.integer "merchant_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "active", default: true
+ end
+
+ add_index "products", ["merchant_id"], name: "index_products_on_merchant_id"
+
+ create_table "reviews", force: :cascade do |t|
+ t.integer "rating"
+ t.string "description"
+ t.integer "product_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+end
diff --git a/db/seeds.rb b/db/seeds.rb
index 4edb1e857e..e578b7e69d 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -5,3 +5,280 @@
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
+merchants = [
+ {
+ user_name: "SSBinks",
+ uid: 16868372,
+ provider: "github",
+ email: "smeggs1@gmail.com",
+ id: 1
+ },
+ {
+ user_name: "sckirk",
+ uid: 17997728,
+ provider: "github",
+ email: "skirk.seattle@gmail.com",
+ id: 2
+ },
+ {
+ user_name: "johnanmorris",
+ uid: 10949311,
+ provider: "github",
+ email: "johna.n.morris@gmail.com",
+ id: 3
+ },
+ {
+ user_name: "yenicapotediaz",
+ uid: 17466680,
+ provider: "github",
+ email: "yenicapote08@gmail.com",
+ id: 4
+ }
+]
+
+merchants.each do |merchant|
+ Merchant.create(merchant)
+end
+
+
+products = [
+ {
+ name: "dragon costume",
+ description: "For your gecko who's always wanted to be a dragon",
+ stock: 48,
+ price: 1798,
+ photo_url: "http://img05.deviantart.net/156d/i/2006/304/e/d/happy_halloween_by_snakeskii.jpg",
+ merchant_id: 1
+ },
+ {
+ name: "cat pants",
+ description: "a nice pair of slacks with an elastic waist",
+ stock: 15,
+ price: 850,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/236x/72/77/86/7277863e594b27677d2228101ef7e293.jpg",
+ merchant_id: 1
+ },
+ {
+ name: "dog booties",
+ description: "a nice pair of slacks with an elastic waist",
+ stock: 40,
+ price: 2026,
+ photo_url: "http://s7d1.scene7.com/is/image/PETCO/2410644-center-1?$ProductList-medium$",
+ merchant_id: 1
+ },
+ {
+ name: "santa gp",
+ description: "For the guinea pig who loves cookies and milk!",
+ stock: 90,
+ price: 1886,
+ photo_url: "http://i.huffpost.com/gadgets/slideshows/318566/slide_318566_2975396_free.jpg",
+ merchant_id: 1
+ },
+ {
+ name: "Pirate Pig",
+ description: "rrrrrrr, matey! Oink!",
+ stock: 40,
+ price: 8678,
+ photo_url: "http://www.whenpigsflynaked.com/uploads/1/1/9/8/11981007/2001761.jpg?276",
+ merchant_id: 1
+ },
+ {
+ name: "llama scarf",
+ description: "long and warm and not made with other llamas",
+ stock: 20,
+ price: 1000,
+ photo_url: "http://www.yesandyes.org/wp-content/uploads/2011/01/scarf5.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "Freddy the Frog - pig",
+ description: "for the pig who longs to be a frog",
+ stock: 200,
+ price: 4898,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/236x/82/cf/6a/82cf6a68cb2a7ee1ecf461efcb445a78.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "pig hoodie",
+ description: "comfortable fleece-lined hoodie for your mini pig",
+ stock: 88,
+ price: 4204,
+ photo_url: "http://66.media.tumblr.com/tumblr_m0h5qg83CY1rr5f7co1_500.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "hedgehog bagpipes",
+ description: "For your hedgehog who loves to Scottish jigs",
+ stock: 20,
+ price: 2695,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/564x/29/8c/a4/298ca4654d55ffbe01aa4afa3a9e66c8.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "pumpkin gp",
+ description: "this little piggy is ready to go trick-or-treating",
+ stock: 60,
+ price: 1995,
+ photo_url: "http://s.marketwatch.com/public/resources/MWimages/MW-BN838_pfpets_MG_20131024180004.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "penguin hawaiian shirt",
+ description: "For the penguin who wants to get away",
+ stock: 4,
+ price: 2000,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/originals/bf/8a/c8/bf8ac87e3ae2ff56ec7db987edd60d7d.jpg",
+ merchant_id: 2
+ },
+ {
+ name: "beaver pipes",
+ description: "For the beaver who's roots are in Scotland",
+ stock: 26,
+ price: 2804,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/236x/e1/36/a7/e136a71ba59f26a4f47bb6e34d2129b5.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "stylish puffy jacket",
+ description: "For the dog who gets a little chilly",
+ stock: 26,
+ price: 48000,
+ photo_url: "http://www.dogsmartway.com/upload/product_ad_1881.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "walk the plank",
+ description: "For the cat who's always in charge",
+ stock: 40,
+ price: 6000,
+ photo_url: "https://d8s0dvdlqz4n8.cloudfront.net/wp-content/uploads/2015/03/Pirate-Cat-Costume1.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "active stripes",
+ description: "For the dog who can't wait to hit the track",
+ stock: 80,
+ price: 2200,
+ photo_url: "https://img0.etsystatic.com/138/0/10819873/il_570xN.1019173514_9lsr.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "top hat for dogs",
+ description: "For the dog who likes nice things",
+ stock: 30,
+ price: 500,
+ photo_url: "http://2damnfunny.com/wp-content/uploads/2013/12/Classy-Dog-With-a-Fancy-Top-Hat-a-Rich-Mahogany-Smoke-Pipe.png",
+ merchant_id: 3
+ },
+ {
+ name: "Pig Cowboy Hat",
+ description: "Yeehawg! Get along, lil piggy!",
+ stock: 20,
+ price: 400,
+ photo_url: "https://67.media.tumblr.com/tumblr_m8bbiv8kil1r9uia2o1_1280.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "piglet dance outfit",
+ description: "For the lil piggies who just wanna dance!",
+ stock: 12,
+ price: 3050,
+ photo_url: "http://www.fashionworld.co.uk/blog/wp-content/uploads/2016/01/Cute-Animals-Wearing-Clothes-Pigs-in-Tutus-Playing-Pets-25.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "weiner dog bobsled",
+ description: "For the dog who craves speed and adventure",
+ stock: 12,
+ price: 36800,
+ photo_url: "http://cdn3-www.dogtime.com/assets/uploads/gallery/wiener-dog-halloween-costumes/8-wiener-dog-bobsled-team.jpg",
+ merchant_id: 3
+ },
+ {
+ name: "Nyan Cat",
+ description: "A pop tart cat inspired costume for your cat to nyan around the house in. Made out of materials that won't harm your pet and safe to lay in as well.",
+ stock: 150,
+ price: 5000,
+ photo_url: "https://img0.etsystatic.com/139/0/10302358/il_570xN.1030148796_77jh.jpg",
+ merchant_id: 4
+ },
+ {
+ name: "Aye aye Captain",
+ description: "For the cat who longs to be at sea.",
+ stock: 60,
+ price: 1800,
+ photo_url: "http://www.momdoesreviews.com/wp-content/uploads/2015/10/Sailor-Dog-Cat-Costume.jpg",
+ merchant_id: 4
+ },
+ {
+ name: "Minnie Pig",
+ description: "For the pig who dreams of the magical land!",
+ stock: 40,
+ price: 11800,
+ photo_url: "http://i.mobofree.com/?u=http%3A%2F%2Flostininternet.com%2Fwp-content%2Fuploads%2F2014%2F10%2FPoppy-and-priscilla-piglets-01.jpg&w=600&h=1500",
+ merchant_id: 4
+ },
+ {
+ name: "Baby Bunny outfit",
+ description: "Are you a new mommy? Dress your little hopper in style!",
+ stock: 4,
+ price: 1900,
+ photo_url: "http://4.bp.blogspot.com/-hZqBuA0W9W8/Uda9I5QVTiI/AAAAAAAABes/Ijk17zrUo-w/s500/tumblr_lsxp1kA2Ks1r4zdm8o1_500.jpg",
+ merchant_id: 4
+ },
+ {
+ name: "trendy mini hat",
+ description: "Halloween hat for geckos...",
+ stock: 46,
+ price: 1996,
+ photo_url: "https://s-media-cache-ak0.pinimg.com/564x/24/a8/d5/24a8d5ad50b163a11766e62af2cde5ce.jpg",
+ merchant_id: 4
+ }
+]
+
+products.each do |item|
+ Product.create(item)
+end
+
+
+
+categories = ["dragon", "pants", "booties", "santa", "pirate", "scarves", "frog", "hoodies", "bagpipes", "pumpkin", "shirts", "beaver", "jackets", "cat", "track suits", "hats", "cowboy hats", "tutus", "bobsleds", "nyan", "sailors", "minnie mouse", "bunny", "leopard print", "mammals", "birds", "amphibians"]
+
+# categories = ["cat", "pants", "dog", "booties", "santa", "guinea pig", "pirate", "pig", "llama", "scarves", "frog", "pig", "pig", "hoodies", "hedgehog", "bagpipes", "pumpkin", "guinea pig", "penguin", "shirts", "beaver", "bagpipes", "dog", "jackets", "pirate", "cat", "dog", "track suits", "hats", "dog", "pig", "cowboy hats", "pig", "tutus", "dog", "bobsleds", "nyan", "cat", "cat", "sailors", "pig", "minnie mouse", "baby", "bunny", "mammals", "birds"]
+
+categories.each do |category|
+ Category.create(name: category)
+end
+
+# My apologies Noelle, I'm adding in more seed product data, so that our views look more robust for our Friday demo.
+
+all_categories = Category.all
+all_products = Product.all
+
+
+# NOTE: The code below only works for the seed! My seed data has 2 categories per product in the seed. If this seed
+# data changes, then this code WILL NOT WORK.
+i = 0
+j = 0
+until i == all_products.size
+ product = all_products[i]
+ all_products[i].categories << all_categories[j]
+ # j += 1
+ # all_products[i].categories << all_categories[j]
+ i+=1
+ j += 1
+end
+
+birds = Category.find_by(name: "birds")
+amphibians = Category.find_by(name: "amphibians")
+mammals = Category.find_by(name: "mammals")
+
+all_products.each do |item|
+ if item.name == "penguin hawaiian shirt"
+ item.categories << birds
+ elsif item.name == "trendy mini hat"
+ item.categories << amphibians
+ else
+ item.categories << mammals
+ end
+end
diff --git a/test/controllers/carts_controller_test.rb b/test/controllers/carts_controller_test.rb
new file mode 100644
index 0000000000..d80b51d923
--- /dev/null
+++ b/test/controllers/carts_controller_test.rb
@@ -0,0 +1,28 @@
+require 'test_helper'
+
+class CartsControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+def cart_create
+ if !session[:cart].nil?
+ @cart = session[:cart]
+ else
+ session[:cart] = []
+ @cart = session[:cart]
+ end
+end
+test "should get the index page" do
+ get :index
+ assert_template :index
+ assert_response :success
+end
+
+ test "Can create a cart" do
+ cart_create
+ assert_difference('@cart.count', 1) do
+ end
+ end
+
+
+end
diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb
new file mode 100644
index 0000000000..7fdce519a4
--- /dev/null
+++ b/test/controllers/categories_controller_test.rb
@@ -0,0 +1,30 @@
+require 'test_helper'
+
+class CategoriesControllerTest < ActionController::TestCase
+
+ test "should get category index" do
+ get :index
+ assert_response :success
+ end
+
+ test "show page should show the proper category" do
+ cat_id = categories(:cat).id
+ get :show, {id: cat_id}
+ assert_response :success
+ assert_template :show
+
+ category = assigns(:category)
+ assert_not_nil category
+ assert_equal category.id, cat_id
+ end
+
+ test "can't show a category that doesn't exist" do
+ cat_id = 12345
+ assert_raises ActiveRecord::RecordNotFound do
+ Category.find(cat_id)
+ end
+
+ get :show, {id: cat_id}
+ assert_response :not_found
+ end
+end
diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb
new file mode 100644
index 0000000000..b230be25cc
--- /dev/null
+++ b/test/controllers/home_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class HomeControllerTest < ActionController::TestCase
+ test "should get index" do
+ get :index
+ assert_response :success
+ end
+end
diff --git a/test/controllers/merchants_controller_test.rb b/test/controllers/merchants_controller_test.rb
new file mode 100644
index 0000000000..7defa7c070
--- /dev/null
+++ b/test/controllers/merchants_controller_test.rb
@@ -0,0 +1,31 @@
+require 'test_helper'
+
+class MerchantsControllerTest < ActionController::TestCase
+ test "should get the index page" do
+ get :index
+ assert_template :index
+ assert_response :success
+ end
+
+ test "show the individual merchant page" do
+ merchant_id = merchants(:hilarious).id
+
+ get :show, {id: merchant_id}
+ assert_response :success
+ assert_template :show
+
+ merchant = assigns(:merchant)
+ assert_not_nil merchant
+ assert_equal merchant.id, merchant_id
+ end
+
+ test "show a merchant that doesn't exist" do
+ merchant_id = 334456777592
+ assert_raises ActiveRecord::RecordNotFound do
+ Merchant.find(merchant_id)
+ end
+
+ get :show, {id: merchant_id}
+ assert_response :not_found
+ end
+end
diff --git a/test/controllers/order_items_controller_test.rb b/test/controllers/order_items_controller_test.rb
new file mode 100644
index 0000000000..2969f1a158
--- /dev/null
+++ b/test/controllers/order_items_controller_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class OrderItemsControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb
new file mode 100644
index 0000000000..a5cad9c248
--- /dev/null
+++ b/test/controllers/orders_controller_test.rb
@@ -0,0 +1,37 @@
+require 'test_helper'
+
+class OrdersControllerTest < ActionController::TestCase
+ test "should get the index page" do
+ get :index
+ assert_template :index
+ assert_response :success
+ end
+
+ test "should get show" do
+ order_id = orders(:valid_order).id
+ get :show, {id: order_id}
+ assert_response :success
+ end
+
+ test "should get create" do
+ order = orders(:valid_order)
+ get :create, {id: order.id}
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get :edit
+ assert_response :success
+ end
+
+ test "should get update" do
+ get :update
+ assert_response :success
+ end
+
+ test "should get destroy" do
+ get :destroy
+ assert_response :success
+ end
+
+end
diff --git a/test/controllers/products_controller_test.rb b/test/controllers/products_controller_test.rb
new file mode 100644
index 0000000000..6be0d6c5d9
--- /dev/null
+++ b/test/controllers/products_controller_test.rb
@@ -0,0 +1,160 @@
+require 'test_helper'
+
+class ProductsControllerTest < ActionController::TestCase
+ test "should get the index page" do
+ get :index
+ assert_template :index
+ assert_response :success
+ end
+
+ test "show should a specific product" do
+ get :show, {id: products(:cat_suit).id}
+ assert_response :success
+ assert_template :show
+ assert_equal assigns(:product), products(:cat_suit)
+ end
+
+ test "should return 404 for a product that doesn't exist" do
+ product_id = 123448658
+ assert_raises ActiveRecord::RecordNotFound do
+ Product.find(product_id)
+ end
+ get :show, {id: product_id}
+ assert_response :not_found
+ end
+
+ test "should get the new form" do
+ merchant = merchants(:doctor)
+ get :new, {user_id: merchant.id}
+ assert_template :new
+ assert_template partial: '_form'
+ assert_response :success
+ end
+
+ test "create should add a new product to the database" do
+ user = merchants(:doctor)
+ post_params = {product: {name: "dog sunglasses", description: "too cool!", price: 549, stock: 10}}
+ assert_difference("Product.count", 1) do
+ post :create, post_params, {user_id: user.id}
+ end
+
+ product = assigns(:product)
+ assert_redirected_to product_path(product.id)
+ end
+
+ test "product with no name can't change the database" do
+ user = merchants(:doctor)
+ post_params = {product: {description: "too cool!", price: 549, stock: 10}}
+ assert_no_difference("Product.count") do
+ post :create, post_params, {user_id: user.id}
+ end
+ assert_template :new
+ end
+
+ test "product with no price can't change the database" do
+ user = merchants(:doctor)
+ post_params = {product: {name: "dog sunglasses", description: "too cool!", stock: 10}}
+ assert_no_difference("Product.count") do
+ post :create, post_params, {user_id: user.id}
+ end
+ assert_template :new
+ end
+
+ test "product with no stock can't change the database" do
+ user = merchants(:doctor)
+ post_params = {product: {name: "dog sunglasses", description: "too cool!"}}
+ assert_no_difference("Product.count") do
+ post :create, post_params, {user_id: user.id}
+ end
+ assert_template :new
+ end
+
+ test "should get the product edit form" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ get :edit, {id: product.id}, {user_id: merchant_id}
+ assert_template :edit
+ assert_template partial: '_form'
+ assert_response :success
+
+ product = assigns(:product)
+ assert_not_nil product
+ assert_equal product.id, product.id
+ end
+
+ test "merchant should not be able to edit another merchant's product" do
+ product = products(:cat_suit)
+ merchant = merchants(:doctor)
+ get :edit, {id: product.id}, {user_id: merchant.id}
+ assert_redirected_to manage_products_path
+ end
+
+ test "update should change the product" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :update, {id: product.id, product: {name: "cat fancy suit"}}, {user_id: merchant_id}
+ assert_equal "cat fancy suit", Product.find(product.id).name
+
+ assert_redirected_to product_path
+ end
+
+ test "update should not allow empty name" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :update, {id: product.id, product: {name: nil}}, {user_id: merchant_id}
+ assert_equal "cat suit", Product.find(product.id).name
+
+ assert_template :edit
+ end
+
+ test "update should not allow invalid price" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :update, {id: product.id, product: {price: 0}}, {user_id: merchant_id}
+ assert_equal 1234, Product.find(product.id).price
+
+ assert_template :edit
+ end
+
+ test "update should not allow empty stock" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :update, {id: product.id, product: {stock: nil}}, {user_id: merchant_id}
+ assert_equal 4, Product.find(product.id).stock
+
+ assert_template :edit
+ end
+
+ test "destroy should delete the product" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ assert_difference("Product.count", -1) do
+ delete :destroy, {id: product.id}, {user_id: merchant_id}
+ end
+
+ assert_raises ActiveRecord::RecordNotFound do
+ Product.find(product.id)
+ end
+
+ assert_redirected_to portal_path
+ end
+
+ test "retire should retire the product if the product is active" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :retire, {id: product.id}, {user_id: merchant_id}
+
+ assert_equal false, Product.find(product.id).active
+
+ assert_redirected_to portal_path
+ end
+
+ test "retire should activate the product if the product is retired" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+ patch :retire, {id: product.id}, {user_id: merchant_id}
+ assert_equal false, Product.find(product.id).active
+ patch :retire, {id: product.id}, {user_id: merchant_id}
+ assert_equal true, Product.find(product.id).active
+ end
+end
diff --git a/test/controllers/reviews_controller_test.rb b/test/controllers/reviews_controller_test.rb
new file mode 100644
index 0000000000..24ff2074e0
--- /dev/null
+++ b/test/controllers/reviews_controller_test.rb
@@ -0,0 +1,46 @@
+require 'test_helper'
+
+class ReviewsControllerTest < ActionController::TestCase
+ test "should be able to view the form to add a new review" do
+ product = products(:cat_suit)
+ ex_review = reviews(:one_star)
+ all_reviews = product.reviews
+
+ get :new, {product_id: product.id}
+ assert_response :success
+ assert_template :new
+ assert_template partial: "_form"
+ end
+
+ test "should be able to save a review to the database" do
+ product = products(:cat_suit)
+ review = { review: {rating: 1, description: "ugh"}, product_id: product.id }
+
+ assert_difference("Review.count", 1) do
+ post :create, review
+ end
+
+ assert_redirected_to product_path(product.id)
+
+ end
+
+ test "should not save an invalid review to the database" do
+ product = products(:cat_suit)
+ review = { review: {rating: 1}, product_id: product.id }
+
+ assert_no_difference("Review.count") do
+ post :create, review
+ end
+
+ assert_template :new
+ end
+
+ test "should not be able to review one's own product" do
+ product = products(:cat_suit)
+ merchant_id = product.merchant_id
+
+ get :new, {product_id: product.id}, {user_id: merchant_id}
+
+ assert_redirected_to product_path(product)
+ end
+end
diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb
new file mode 100644
index 0000000000..b76f3f77bf
--- /dev/null
+++ b/test/controllers/sessions_controller_test.rb
@@ -0,0 +1,38 @@
+require 'test_helper'
+
+class SessionsControllerTest < ActionController::TestCase
+ def login_a_user
+ request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
+ get :create, {provider: "github"}
+ end
+
+ test "Can Create a user" do
+ assert_difference('Merchant.count', 1) do
+ login_a_user
+ assert_response :redirect
+ assert_redirected_to portal_path
+ end
+ end
+
+ test "If a user logs in twice it doesn't create a 2nd user" do
+ assert_difference('Merchant.count', 1) do
+ login_a_user
+ end
+
+ assert_not_nil session[:user_id]
+
+ assert_no_difference('Merchant.count') do
+ login_a_user
+ assert_response :redirect
+ assert_redirected_to portal_path
+ assert_not_nil session[:user_id]
+ end
+ end
+
+ # test "a logged-in user can view the index" do
+ # login_a_user
+ # assert_not_nil session[:user_id]
+ # assert_response :success
+ # assert_template :index
+ # end
+end
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
new file mode 100644
index 0000000000..b08e755443
--- /dev/null
+++ b/test/fixtures/categories.yml
@@ -0,0 +1,10 @@
+cat:
+ name: cat
+formal_wear:
+ name: formal wear
+hamster:
+ name: hamster
+eyewear:
+ name: eyewear
+mammals:
+ name: mammals
diff --git a/test/fixtures/merchants.yml b/test/fixtures/merchants.yml
new file mode 100644
index 0000000000..b1c3150bb4
--- /dev/null
+++ b/test/fixtures/merchants.yml
@@ -0,0 +1,15 @@
+hilarious:
+ user_name: Hillary Oss
+ email: hillary@oss.com
+ uid: 125673
+ provider: github
+chanda:
+ user_name: Chanda Lear
+ email: chandelier@gmail.com
+ uid: 67890
+ provider: github
+doctor:
+ user_name: The Doctor
+ email: doctorwho@tardis.net
+ uid: 42
+ provider: github
diff --git a/test/fixtures/order_items.yml b/test/fixtures/order_items.yml
new file mode 100644
index 0000000000..95b93accf6
--- /dev/null
+++ b/test/fixtures/order_items.yml
@@ -0,0 +1,10 @@
+one_unit:
+ quantity: 1
+ product: cat_suit
+ order_id: 456
+ shipped?: true
+four_unit:
+ quantity: 4
+ product: cat_suit
+ order: valid_order
+ shipped?: true
diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml
new file mode 100644
index 0000000000..1be061624c
--- /dev/null
+++ b/test/fixtures/orders.yml
@@ -0,0 +1,4 @@
+valid_order:
+ cc_number: 6789
+ cc_exp_year: 2016
+ cc_exp_month: 12
diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml
new file mode 100644
index 0000000000..441731bfb8
--- /dev/null
+++ b/test/fixtures/products.yml
@@ -0,0 +1,28 @@
+cat_suit:
+ name: cat suit
+ price: 1234
+ description: your cat will look dapper in this one-piece suit!
+ stock: 4
+ photo_url: http://placekitten.com/200/300
+ categories: cat, formal_wear, mammals
+ merchant: hilarious
+ active: true
+
+hamster_monocle:
+ name: hamster monocle
+ price: 500
+ description: a classy way to correct your hamster's vision.
+ stock: 20
+ photo_url: http://placekitten.com/200/300
+ categories: hamster, eyewear, mammals
+ merchant: hilarious
+ active: true
+
+dog_bunny_ears:
+ name: Dog bunny ears
+ price: 3453
+ description: make your dog look like a bunny with these adorable bunny ears!
+ stock: 1
+ photo_url: http://placekitten.com/200/300
+ merchant: doctor
+ active: true
diff --git a/test/fixtures/reviews.yml b/test/fixtures/reviews.yml
new file mode 100644
index 0000000000..ba9fb29869
--- /dev/null
+++ b/test/fixtures/reviews.yml
@@ -0,0 +1,16 @@
+one_star:
+ rating: 1
+ description: This product is terrible!
+ product: cat_suit
+four_star:
+ rating: 4
+ description: Highly recommend purchasing this item.
+ product: hamster_monocle
+five_star:
+ rating: 5
+ description: Another description...
+ product: dog_bunny_ears
+character_400:
+ rating: 4
+ description: Here is a review of a book for sale. It is a great book, worth reading and possibly adding to your personal library. You will not be disappointed. Check this book out from your local library so you can form your own opinion and upvote it here. klsfjshrjdf lkajsf jej faiosejf esj oiasejf lsdjf ;oasjef ojsdfkl jasefj sodf lsdfj oisejf SDJf lKSDJFo iejw lKDSFJ l;SEF SIefj l;KSZDf sd eddjjrhdm fydowkf
+ product: cat_suit
diff --git a/test/models/category_test.rb b/test/models/category_test.rb
new file mode 100644
index 0000000000..cc4feecad5
--- /dev/null
+++ b/test/models/category_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class CategoryTest < ActiveSupport::TestCase
+ test "Category can't have an empty name" do
+ category = Category.new
+ assert_not category.valid?
+ assert_not category.save
+ assert_includes category.errors, :name
+ end
+
+ test "Category is valid if it has a name" do
+ categories.each do |category|
+ category = Category.create!(name: category.name)
+ assert category.valid?
+ assert_not_nil category.name
+ end
+ end
+
+ test "Category name must be unique" do
+ category = Category.new(name: "formal wear")
+ assert_not category.valid?
+ assert_not category.save
+ assert_equal ["has already been taken"], category.errors.messages[:name]
+ end
+
+ test "Category can have many products" do
+ category = categories(:mammals)
+ product_one = products(:cat_suit)
+ product_two = products(:hamster_monocle)
+
+ assert_equal 2, category.products.length
+ assert_includes category.product_ids, product_one.id
+ assert_includes category.product_ids, product_two.id
+ assert_respond_to category, :products
+ end
+end
diff --git a/test/models/merchant_test.rb b/test/models/merchant_test.rb
new file mode 100644
index 0000000000..0af2333c8f
--- /dev/null
+++ b/test/models/merchant_test.rb
@@ -0,0 +1,19 @@
+require 'test_helper'
+
+class MerchantTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+ test "create a merchant with no email, name or uid" do
+ merchant = Merchant.new
+ assert_not merchant.valid?
+ end
+
+ test "the Merchant will not be valid without an email" do
+ git_hash = {uid: 15, provider: "twitter",
+ info: {nickname: 'kitty'}}
+ merchant = Merchant.build_from_github(git_hash)
+ assert_not merchant.valid?
+
+ end
+end
diff --git a/test/models/order_item_test.rb b/test/models/order_item_test.rb
new file mode 100644
index 0000000000..155d44c124
--- /dev/null
+++ b/test/models/order_item_test.rb
@@ -0,0 +1,53 @@
+require 'test_helper'
+
+class OrderItemTest < ActiveSupport::TestCase
+ test "Cannot create an order item with nil quantity" do
+ nil_quantity = OrderItem.new(quantity: nil, product: products(:cat_suit), order_id: 456)
+ assert_equal(nil, nil_quantity.quantity)
+ assert_not nil_quantity.valid?
+ assert_includes(nil_quantity.errors, :quantity)
+ end
+
+ test "Order item quantity is always instantiated at 1" do
+ default_quantity = OrderItem.new(product: products(:cat_suit), order_id: 456)
+ assert_equal(1, default_quantity.quantity)
+ end
+
+ test "Order item quantity must be a positive (non-zero) integer (LESS THAN OR EQUAL TO THE PRODUCT'S QUANTITY/STOCK, WOULD lIKE TO BUILD IN THIS PART OF THE TEST)" do
+ assert order_items(:four_unit).valid?, "Validation failed: #{order_items(:four_unit).errors.messages}"
+ assert order_items(:one_unit).valid?
+
+ zero_quantity = OrderItem.new(quantity: 0, product: products(:cat_suit), order_id: 456)
+ assert_not zero_quantity.valid?
+ assert_includes(zero_quantity.errors, :quantity)
+
+ negative_quantity = OrderItem.new(quantity: -6, product: products(:cat_suit), order_id: 456)
+ assert_not negative_quantity.valid?
+ assert_includes(negative_quantity.errors, :quantity)
+ end
+
+ test "Order item creation requires a product_id [integer]" do
+ no_product_id = OrderItem.new(quantity: 2, order_id: 456)
+ assert_not no_product_id.valid?
+ assert_includes(no_product_id.errors, :product_id)
+
+ assert_equal(products(:cat_suit).id, order_items(:one_unit).product_id)
+ assert order_items(:four_unit).valid?
+ end
+
+ test "Order item creation requires an order_id [integer]" do
+ no_order_id = OrderItem.new(quantity: 2, product: products(:cat_suit))
+ assert_not no_order_id.valid?
+ assert_includes(no_order_id.errors, :order_id)
+
+ assert order_items(:one_unit).valid?
+ assert order_items(:four_unit).valid?
+ end
+
+ test "Creating an order item instantiates :shipped? as false" do
+ o = OrderItem.new
+ assert_equal(false, o.shipped?)
+ end
+end
+
+ # fyi from earlier project: on rails console, object.new goes straight to the model, bypassing the controller completely.
diff --git a/test/models/order_test.rb b/test/models/order_test.rb
new file mode 100644
index 0000000000..7b03395764
--- /dev/null
+++ b/test/models/order_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class OrderTest < ActiveSupport::TestCase
+ test "credit card number must be present and be 4 characters long" do
+ order2 = Order.new(cc_number: 34, cc_exp_year: 1990, cc_exp_month: 7)
+ order3 = Order.new(cc_number: 6789234, cc_exp_year: 1990, cc_exp_month: 7)
+
+ assert orders(:valid_order).valid?
+ assert_not order2.valid?
+ assert_not order3.valid?
+ end
+
+ test "credit card should not have an expired date" do
+ expired_year = Order.new(cc_number: 1234, cc_exp_year: (Time.now.year - 2), cc_exp_month: 12)
+ expired_month_in_current_year = Order.new(cc_number: 1234, cc_exp_year: Time.now.year, cc_exp_month: (Time.now.month - 2))
+
+ assert_not expired_year.valid?
+ assert_not expired_month_in_current_year.valid?
+ end
+
+ test "an order model object's status defaults to PENDING upon being saved to the database" do
+ o = orders(:valid_order)
+ o.save
+ assert_equal("PENDING", o.status)
+ end
+
+ test "confirming the private method update_total occurs on an order object" do
+ o = Order.new(cc_number: 6789, cc_exp_year: 2016, cc_exp_month: 12)
+ assert_equal(0, o.total)
+ assert o.save
+ p = o.order_items.new(quantity: 4, product: products(:cat_suit), shipped?: true)
+ assert p.save
+ assert_equal(4936, o.total)
+ end
+
+end
diff --git a/test/models/product_test.rb b/test/models/product_test.rb
new file mode 100644
index 0000000000..a5d0d008da
--- /dev/null
+++ b/test/models/product_test.rb
@@ -0,0 +1,108 @@
+require 'test_helper'
+
+class ProductTest < ActiveSupport::TestCase
+ test "Cannot create a product without a name" do
+ product = Product.new(stock: 2, price: 400)
+ assert_not product.valid?
+ assert_includes product.errors, :name
+ end
+
+ test "Cannot create a product without a price" do
+ product = Product.new(name: "kitten tux", stock: 2)
+ assert_not product.valid?
+ assert_includes product.errors, :price
+ end
+
+ test "Cannot create a product with a duplicate name" do
+ product = Product.new(name: "cat suit", price: 224, stock: 5)
+ assert_not product.valid?
+ assert_not product.save
+ assert_includes product.errors, :name
+ assert_equal ["has already been taken"], product.errors.messages[:name]
+ end
+
+ test "Price must be an integer" do
+ product = Product.new(name: "dog sunglasses", price: "foo", stock: 1)
+ assert_not product.valid?
+ assert_not product.save
+ assert_equal ["is not a number"], product.errors.messages[:price]
+ end
+
+ test "Price can't be 0" do
+ product = Product.new(name: "foobar", price: 0, stock: 1)
+ assert_not product.valid?
+ assert_not product.save
+ assert_equal ["must be greater than 0"], product.errors.messages[:price]
+ end
+
+ test "Price can't be less than 0" do
+ product = Product.new(name: "foobar", price: -1, stock: 1)
+ assert_not product.valid?
+ assert_not product.save
+ assert_equal ["must be greater than 0"], product.errors.messages[:price]
+ end
+
+ test "Stock can't be less than 0" do
+ product = Product.new(name: "foobar", price: 500, stock: -10)
+ assert_not product.valid?
+ assert_not product.save
+ assert_equal ["must be greater than or equal to 0"], product.errors.messages[:stock]
+ end
+
+ test "Stock can be zero" do
+ product = Product.new(name: "foobar", price: 120, stock: 0)
+ assert product.valid?
+ assert_not_nil product.stock
+ assert product.save
+ end
+
+ test "create Product with valid data" do
+ product = products(:cat_suit)
+
+ assert product.valid?
+ assert_not_nil product.name
+ assert_not_nil product.price
+ assert_not_nil product.stock
+ end
+
+ test "create products with different names" do
+ products.each do |product|
+ assert product.valid?
+ assert product.save
+ end
+ end
+
+ test "Product belongs to a merchant" do
+ product = Product.create!(name: "mouse hat", price: 1240, stock: 5)
+ merchant = Merchant.create!(user_name: "testing", email: "test@test.com", uid: 124, provider: "github")
+
+ product.merchant = merchant
+ assert product.save
+
+ assert_equal product.merchant_id, merchant.id
+ assert_includes merchant.products, product
+ assert_respond_to product, :merchant
+ end
+
+ test "Product can have many categories" do
+ product = products(:hamster_monocle)
+ category_one = categories(:hamster)
+ category_two = categories(:eyewear)
+ category_three = categories(:mammals)
+ assert_equal 3, product.categories.length
+ assert_includes product.category_ids, category_one.id
+ assert_includes product.category_ids, category_two.id
+ assert_includes product.category_ids, category_three.id
+ assert_respond_to product, :categories
+ end
+
+ test "Products can have many order items" do
+ product = products(:hamster_monocle)
+ assert_respond_to product, :order_items
+ end
+
+ test "Products can have many reviews" do
+ product = products(:cat_suit)
+ assert_respond_to product, :reviews
+ end
+end
diff --git a/test/models/review_test.rb b/test/models/review_test.rb
new file mode 100644
index 0000000000..25077ad0b9
--- /dev/null
+++ b/test/models/review_test.rb
@@ -0,0 +1,70 @@
+require 'test_helper'
+
+class ReviewTest < ActiveSupport::TestCase
+ test "Creating a new review will instantiate rating as nil" do
+ r = Review.new
+ assert_equal(nil, r.rating)
+ end
+
+ test "Cannot save a nil rating to the database" do
+ no_rating = Review.new(description: "whatever", product_id: 123)
+ assert_not no_rating.valid?
+ assert_includes(no_rating.errors, :rating)
+ end
+
+ test "Can create reviews with ratings between 1 and 5" do
+ assert reviews(:one_star).valid?
+ assert reviews(:four_star).valid?
+ assert reviews(:five_star).valid?
+ end
+
+ test "Can only create reviews with integer ratings" do
+ float_rating = Review.new(rating: 3.2, description: "whatever", product_id: 123)
+ assert_not float_rating.valid?
+ assert_includes(float_rating.errors, :rating)
+
+ string_rating = Review.new(rating: "excellent", description: "whatever", product_id: 123)
+ assert_not string_rating.valid?
+ assert_includes(string_rating.errors, :rating)
+ end
+
+ test "Cannot create reviews with ratings less than 1 or greater than 5" do
+ too_low_rating = Review.new(rating: 0, description: "whatever", product_id: 123)
+ assert_not too_low_rating.valid?
+ assert_includes(too_low_rating.errors, :rating)
+
+ too_high_rating = Review.new(rating: 6, description: "whatever", product_id: 123)
+ assert_not too_high_rating.valid?
+ assert_includes(too_high_rating.errors, :rating)
+ end
+
+ test "Review creation requires a description [string]" do
+ assert reviews(:one_star).valid?
+
+ no_description = Review.new(rating: 0, product_id: 123)
+ assert_not no_description.valid?
+ assert_includes(no_description.errors, :description)
+ end
+
+ test "Can only create reviews with descriptions 400 characters or less" do
+ assert reviews(:character_400).valid?
+
+ character_401 = Review.new(rating: 0, product_id: 123, description: "Here is a review of a book for sale. It is a great book, worth reading and possibly adding to your personal library. You will not be disappointed. Check this book out from your local library so you can form your own opinion and upvote it here. klsfjshrjdf lkajsf jej faiosejf esj oiasejf lsdjf ;oasjef ojsdfkl jasefj sodf lsdfj oisejf SDJf lKSDJFo iejw lKDSFJ l;SEF SIefj l;KSZDf sd eddjjrhdm fydowkf!")
+ assert_not character_401.valid?
+ assert_includes(character_401.errors, :description)
+ end
+
+ test "Review creation requires a product_id [integer]" do
+ assert reviews(:one_star).valid?
+
+ no_product_id = Review.new(rating: 0, description: "hello")
+ assert_not no_product_id.valid?
+ assert_includes(no_product_id.errors, :product_id)
+ end
+
+ test "Review belongs to a product" do
+ review = reviews(:one_star)
+
+ assert_respond_to review, :product
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 92e39b2d78..f782ad5465 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,10 +1,20 @@
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
+require 'simplecov'
+SimpleCov.start 'rails'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
+ Minitest::Reporters.use!
+ def setup
- # Add more helper methods to be used by all tests here...
+ OmniAuth.config.test_mode = true
+
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new({
+ provider: 'github', uid: '123545', info: { email: "a@b.com", nickname: "Ada" }
+ })
+ end
+ # Add more helper methods to be used by all tests here...
end