diff --git a/Gemfile b/Gemfile index 8bd4dbb4f..ec41d2ff7 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'redis', '~>3.2' gem 'sidekiq' gem 'sidekiq-scheduler', '~> 2.1.4' gem 'simple_form' +gem 'font-awesome-rails' gem 'courses', path: 'components/courses' gem 'shop', path: 'components/shop' diff --git a/Gemfile.lock b/Gemfile.lock index 703e125f2..57febba2f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -226,6 +226,8 @@ GEM faraday (1.0.1) multipart-post (>= 1.2, < 3) ffi (1.12.2) + font-awesome-rails (4.7.0.7) + railties (>= 3.2, < 7) formatador (0.2.5) friendly_id (5.3.0) activerecord (>= 4.0.0) @@ -650,6 +652,7 @@ DEPENDENCIES ez-settings factory_bot_rails faker + font-awesome-rails friendly_id (~> 5.1) gibbon (~> 3.0) groupdate (~> 4.0) diff --git a/app/assets/stylesheets/app/application.scss b/app/assets/stylesheets/app/application.scss index 784c887b6..699553472 100644 --- a/app/assets/stylesheets/app/application.scss +++ b/app/assets/stylesheets/app/application.scss @@ -14,6 +14,7 @@ @import "https://fonts.googleapis.com/css2?family=Oswald&display=swap'"; @import "local_fonts"; +@import "font-awesome"; @import "normalize"; diff --git a/app/assets/stylesheets/app/base/_base.scss b/app/assets/stylesheets/app/base/_base.scss index e13171aa0..00a4046df 100644 --- a/app/assets/stylesheets/app/base/_base.scss +++ b/app/assets/stylesheets/app/base/_base.scss @@ -134,3 +134,28 @@ q { .pk-no-display { display: none !important; } + +.shopping_cart-icon { + display: inline-block; + position: fixed; + left: 1450px; + opacity: 80%; + + &:hover { + opacity: 100%; + } +} + +.cart_table { + margin: 40px; + + table { + border-collapse: collapse; + width: 100%; + } + + th, td { + text-align: left; + padding: 8px; + } +} \ No newline at end of file diff --git a/app/helpers/admin/events_helper.rb b/app/helpers/admin/events_helper.rb index cbe427952..08621ae74 100644 --- a/app/helpers/admin/events_helper.rb +++ b/app/helpers/admin/events_helper.rb @@ -42,7 +42,6 @@ def event_status_label(event) class: ['ui label', BG_STATUS_CLASS[event.status.to_sym]] end - # rubocop:disable Metrics/AbcSize def event_visitors(event) requested = event.pending_visit_requests.length approved = event.approved_visit_requests.length @@ -56,7 +55,6 @@ def event_visitors(event) t('events.index.visitors.visited') => visited } end - # rubocop:enable Metrics/AbcSize def event_verified_user_data(event) verified = event.verified_visitors.length diff --git a/app/views/layouts/application.slim b/app/views/layouts/application.slim index 1d80e12f7..27ed3352f 100644 --- a/app/views/layouts/application.slim +++ b/app/views/layouts/application.slim @@ -13,6 +13,7 @@ html link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16" link rel="manifest" href="/manifest.json" link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" + link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css" == render 'layouts/ga/head' if Rails.env.production? body.pk-main-layout class="#{yield(:main_body_class)}" diff --git a/components/shop/app/controllers/shop/base_controller.rb b/components/shop/app/controllers/shop/base_controller.rb index bc4a29bb8..7fde64dba 100644 --- a/components/shop/app/controllers/shop/base_controller.rb +++ b/components/shop/app/controllers/shop/base_controller.rb @@ -4,6 +4,14 @@ module Shop class BaseController < ApplicationController before_action :authenticate_user! + helper_method :current_order + + def current_order + return unless session[:order_id] + + @current_order ||= Shop::Order.find_by(id: session[:order_id]) + end + private def render_form diff --git a/components/shop/app/controllers/shop/order_items_controller.rb b/components/shop/app/controllers/shop/order_items_controller.rb new file mode 100644 index 000000000..406905211 --- /dev/null +++ b/components/shop/app/controllers/shop/order_items_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Shop + class OrderItemsController < BaseController + helper_method :item, :order_items + + def create + order_item = ::Shop::OrderItem.new(item_id: item.id, order_id: order_id, price: item.price) + order_item.save + + redirect_to shop_items_path + end + + def update + OrderItemService.new(order_item).increase_quantity! + redirect_to shop_order_items_path + end + + def destroy + OrderItemService.new(order_item).reduce_quantity! + redirect_to shop_order_items_path + end + + private + + def order_items + @order_items ||= Shop::OrderItem.includes(:item).map do |order_item| + Shop::OrderItemDecorator.new(order_item) + end + end + + def order_item + @order_items ||= Shop::OrderItem.find(params[:id]) + end + + def item + @items ||= Shop::Item.find(params[:item_id]) + end + + def order_id + session[:order_id] ||= Shop::Order.create.id + end + end +end diff --git a/components/shop/app/decorators/shop/base_decorator.rb b/components/shop/app/decorators/shop/base_decorator.rb new file mode 100644 index 000000000..190764d04 --- /dev/null +++ b/components/shop/app/decorators/shop/base_decorator.rb @@ -0,0 +1,4 @@ +module Shop + class BaseDecorator < SimpleDelegator + end +end diff --git a/components/shop/app/decorators/shop/order_item_decorator.rb b/components/shop/app/decorators/shop/order_item_decorator.rb new file mode 100644 index 000000000..aef29d376 --- /dev/null +++ b/components/shop/app/decorators/shop/order_item_decorator.rb @@ -0,0 +1,7 @@ +module Shop + class OrderItemDecorator < BaseDecorator + def sum + price * quantity + end + end +end diff --git a/components/shop/app/helpers/shop/item_helper.rb b/components/shop/app/helpers/shop/item_helper.rb new file mode 100644 index 000000000..4913c5ad9 --- /dev/null +++ b/components/shop/app/helpers/shop/item_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Shop + module ItemHelper + end +end diff --git a/components/shop/app/models/shop/item.rb b/components/shop/app/models/shop/item.rb index daa6bdf13..d29ded302 100644 --- a/components/shop/app/models/shop/item.rb +++ b/components/shop/app/models/shop/item.rb @@ -5,6 +5,7 @@ class Item < ApplicationRecord self.table_name = 'shop_items' has_many :item_images + has_many :order_items accepts_nested_attributes_for :item_images scope :active, -> { where(published: true) } diff --git a/components/shop/app/models/shop/order.rb b/components/shop/app/models/shop/order.rb new file mode 100644 index 000000000..ceb328584 --- /dev/null +++ b/components/shop/app/models/shop/order.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# status: shopping_cart, placed, processing, shipping, complete, canceled +# user: optional +# buyer: anonymous user +# shipment_address + +module Shop + class Order < ApplicationRecord + self.table_name = 'shop_orders' + + enum status: { shopping_cart: 0, placed: 1, processing: 2, shipping: 3, complete: 4, canceled: 5 } + + has_many :order_items + has_many :items, through: :order_items + + # TODO: OrderDecorator + # def total_sum + # order_items.map(&:price).sum.to_f + # end + + # def order_items_count + # order_items.count + # end + end +end diff --git a/components/shop/app/models/shop/order_item.rb b/components/shop/app/models/shop/order_item.rb new file mode 100644 index 000000000..16af4dcf6 --- /dev/null +++ b/components/shop/app/models/shop/order_item.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Shop + class OrderItem < ApplicationRecord + self.table_name = 'shop_order_items' + + validates_uniqueness_of :item_id + + belongs_to :order + belongs_to :item + end +end diff --git a/components/shop/app/services/shop/order_item_service.rb b/components/shop/app/services/shop/order_item_service.rb new file mode 100644 index 000000000..eeb891fd9 --- /dev/null +++ b/components/shop/app/services/shop/order_item_service.rb @@ -0,0 +1,21 @@ +module Shop + class OrderItemService + attr_reader :order_item + + def initialize(order_item) + @order_item = order_item + end + + def increase_quantity! + order_item.increment!(:quantity) + end + + def reduce_quantity! + if order_item.quantity > 1 + order_item.decrement!(:quantity) + else + order_item.destroy! + end + end + end +end diff --git a/components/shop/app/uploaders/item_image_uploader.rb b/components/shop/app/uploaders/item_image_uploader.rb index 9e3f8f8ae..99745b955 100644 --- a/components/shop/app/uploaders/item_image_uploader.rb +++ b/components/shop/app/uploaders/item_image_uploader.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ItemImageUploader < CarrierWave::Uploader::Base -end \ No newline at end of file +end diff --git a/components/shop/app/views/shop/items/index.slim b/components/shop/app/views/shop/items/index.slim index d69d3fb48..e3224d4d9 100644 --- a/components/shop/app/views/shop/items/index.slim +++ b/components/shop/app/views/shop/items/index.slim @@ -1,5 +1,13 @@ h2.main-header Shop +- if current_order.present? + span.shopping_cart-icon + = link_to shop_order_items_path do + i.fas.fa-shopping-cart.fa-8x + div.item_counts + span + = current_order.order_items_count + .main ul.cards - items.each do |item| @@ -9,4 +17,9 @@ h2.main-header Shop .card_content h2.card_title = item.name p.card_text = item.description - button.btn.card_btn Add to cart + - if current_order.present? && current_order.items.include?(item) + = link_to shop_order_items_path do + button.btn.card_btn Added to the cart + - else + = link_to shop_order_items_path(item_id: item.id), method: :post do + button.btn.card_btn Add to cart diff --git a/components/shop/app/views/shop/order_items/index.slim b/components/shop/app/views/shop/order_items/index.slim new file mode 100644 index 000000000..95fee80f9 --- /dev/null +++ b/components/shop/app/views/shop/order_items/index.slim @@ -0,0 +1,32 @@ +h2.main-header Cart + +.cart_table + - if current_order.present? && current_order.order_items.any? + table + thead + tr + th Image + th.company Name + th.company Price + th.count Count + tbody + - order_items.each do |item| + tr + td= image_tag item.item.item_images.first.image_url(:small).to_s, class: 'card_image' if item.item.item_images.first.try(:image_url) + td= item.item.name + td= item.sum + td + div + = link_to shop_order_item_path(item.id), method: :delete do + i.fas.fa-minus + + = item.quantity + + = link_to shop_order_item_path(item.id), method: :put do + i.fas.fa-plus + + .total_sum + h2 Total + h2= current_order.total_sum + - else + h1 Your cart is empty for now diff --git a/components/shop/config/routes.rb b/components/shop/config/routes.rb index 1a826fb9d..b1c6ad8f6 100644 --- a/components/shop/config/routes.rb +++ b/components/shop/config/routes.rb @@ -3,6 +3,7 @@ Rails.application.routes.draw do namespace :shop do resources :items, only: %i[index] + resources :order_items, only: %i[index create update destroy] end namespace :admin do diff --git a/components/shop/db/migrate/20210611210804_create_orders.rb b/components/shop/db/migrate/20210611210804_create_orders.rb new file mode 100644 index 000000000..3ce78d97d --- /dev/null +++ b/components/shop/db/migrate/20210611210804_create_orders.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CreateOrders < ActiveRecord::Migration[5.2] + def change + create_table :shop_orders do |t| + t.integer :status, default: 0 + t.text :shipment_address + end + end +end diff --git a/components/shop/db/migrate/20210611211002_create_order_items.rb b/components/shop/db/migrate/20210611211002_create_order_items.rb new file mode 100644 index 000000000..c8a214828 --- /dev/null +++ b/components/shop/db/migrate/20210611211002_create_order_items.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateOrderItems < ActiveRecord::Migration[5.2] + def change + create_table :shop_order_items do |t| + t.integer :quantity, default: 1 + t.decimal :price, null: false + t.references :item + t.references :order + end + end +end diff --git a/components/shop/shop.gemspec b/components/shop/shop.gemspec index 8f747a731..51f55983a 100644 --- a/components/shop/shop.gemspec +++ b/components/shop/shop.gemspec @@ -29,11 +29,11 @@ Gem::Specification.new do |spec| spec.add_dependency 'rails', '~> 5.2.2', '>= 5.2.2.1' + spec.add_dependency 'carrierwave', '~> 1.2' spec.add_dependency 'friendly_id' spec.add_dependency 'kaminari' spec.add_dependency 'simple_form' spec.add_dependency 'slim-rails' - spec.add_dependency 'carrierwave', '~> 1.2' spec.add_development_dependency 'capybara' spec.add_development_dependency 'faker' diff --git a/components/shop/spec/dummy/app/assets/stylesheets/application.css b/components/shop/spec/dummy/app/assets/stylesheets/application.css index 0ebd7fe82..3e01e3d08 100644 --- a/components/shop/spec/dummy/app/assets/stylesheets/application.css +++ b/components/shop/spec/dummy/app/assets/stylesheets/application.css @@ -12,4 +12,5 @@ * *= require_tree . *= require_self + *= require font-awesome */ diff --git a/components/shop/spec/dummy/db/schema.rb b/components/shop/spec/dummy/db/schema.rb index 285f33b28..12bb42d0c 100644 --- a/components/shop/spec/dummy/db/schema.rb +++ b/components/shop/spec/dummy/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_210_514_113_103) do +ActiveRecord::Schema.define(version: 20_210_611_211_002) do create_table 'shop_item_images', force: :cascade do |t| t.string 'image' t.integer 'item_id' @@ -30,4 +30,18 @@ t.datetime 'created_at', null: false t.datetime 'updated_at', null: false end + + create_table 'shop_order_items', force: :cascade do |t| + t.integer 'quantity' + t.decimal 'price', null: false + t.integer 'item_id' + t.integer 'order_id' + t.index ['item_id'], name: 'index_shop_order_items_on_item_id' + t.index ['order_id'], name: 'index_shop_order_items_on_order_id' + end + + create_table 'shop_orders', force: :cascade do |t| + t.integer 'status', default: 0 + t.text 'shipment_address' + end end diff --git a/components/shop/spec/factories/order_factory.rb b/components/shop/spec/factories/order_factory.rb new file mode 100644 index 000000000..1811b47e5 --- /dev/null +++ b/components/shop/spec/factories/order_factory.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :order, class: Shop::Order do + shipment_address { 'Lviv, Pivorak street' } + end +end diff --git a/components/shop/spec/factories/order_item_factory.rb b/components/shop/spec/factories/order_item_factory.rb new file mode 100644 index 000000000..ce7fdc606 --- /dev/null +++ b/components/shop/spec/factories/order_item_factory.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :order_item, class: Shop::OrderItem do + price { 12 } + end +end diff --git a/components/shop/spec/features/shop/add_to_cart_spec.rb b/components/shop/spec/features/shop/add_to_cart_spec.rb index 5179920ef..da51b5e6d 100644 --- a/components/shop/spec/features/shop/add_to_cart_spec.rb +++ b/components/shop/spec/features/shop/add_to_cart_spec.rb @@ -1,2 +1,16 @@ # frozen_string_literal: true -# TODO + +require 'rails_helper' + +RSpec.describe '/shop' do + let!(:active_item) { create(:item, published: true) } + + scenario 'display items' do + visit shop_items_path + + click_button 'Add to cart' + + expect(page).to have_current_path '/shop/items' + expect(page).to have_button('Added to the cart') + end +end diff --git a/components/shop/spec/features/shop/home_page_spec.rb b/components/shop/spec/features/shop/home_page_spec.rb index c7ff126f1..8e266a2e4 100644 --- a/components/shop/spec/features/shop/home_page_spec.rb +++ b/components/shop/spec/features/shop/home_page_spec.rb @@ -8,10 +8,10 @@ let!(:item_b) { create(:item, published: false) } scenario 'display items' do - visit admin_shop_items_path + visit shop_items_path expect(page).to have_content item_a.name - expect(page).to have_content item_b.name + expect(page).to have_button('Add to cart') end end end diff --git a/components/shop/spec/features/shop/shopping_cart_spec.rb b/components/shop/spec/features/shop/shopping_cart_spec.rb index 464090415..944671411 100644 --- a/components/shop/spec/features/shop/shopping_cart_spec.rb +++ b/components/shop/spec/features/shop/shopping_cart_spec.rb @@ -1 +1,23 @@ -# TODO +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Shop Cart list' do + let!(:active_item) { create(:item, published: true) } + let!(:order) { create(:order) } + let!(:order_item) { create(:order_item, order: order, item: active_item) } + + fscenario 'display items' do + # TODO: Mock controller session object + # session[:order_id] = order.id + visit shop_order_items_path + + expect(page).to have_content active_item.name + end + + scenario 'display empty cart' do + visit shop_order_items_path + + expect(page).not_to have_content active_item.name + end +end diff --git a/components/shop/spec/services/shop/order_item_service_spec.rb b/components/shop/spec/services/shop/order_item_service_spec.rb new file mode 100644 index 000000000..fa7e8f077 --- /dev/null +++ b/components/shop/spec/services/shop/order_item_service_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe Shop::OrderItemService do + let(:order) { create(:order) } + let(:item) { create(:item) } + let!(:order_item) { create(:order_item, quantity: 1, order: order, item: item) } + + describe '#increase_quantity!' do + subject { described_class.new(order_item).increase_quantity! } + + it 'increase order item qty' do + expect { subject }.to change { order_item.quantity }.from(1).to(2) + end + end + + describe '#reduce_quantity!' do + subject { described_class.new(order_item).reduce_quantity! } + + context 'when > 1' do + let(:order_item) { create(:order_item, quantity: 3, order: order, item: item) } + + it 'increase order item qty' do + expect { subject }.to change { order_item.quantity }.from(3).to(2) + end + end + + context 'when eq 1' do + it 'increase order item qty' do + expect { subject }.to change { Shop::OrderItem.count }.from(1).to(0) + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index dd483650b..b6cfa8be6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_05_14_113103) do +ActiveRecord::Schema.define(version: 2021_06_11_211002) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -252,6 +252,20 @@ t.datetime "updated_at", null: false end + create_table "shop_order_items", force: :cascade do |t| + t.integer "quantity", default: 1 + t.decimal "price", null: false + t.bigint "item_id" + t.bigint "order_id" + t.index ["item_id"], name: "index_shop_order_items_on_item_id" + t.index ["order_id"], name: "index_shop_order_items_on_order_id" + end + + create_table "shop_orders", force: :cascade do |t| + t.integer "status", default: 0 + t.text "shipment_address" + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type"