From cfd21adb60b57b5a028df4addd697b28c8e6e036 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Sat, 20 Aug 2022 18:47:15 +0200 Subject: [PATCH] cart button --- .../components/cart/cart-button.tsx | 48 +++++++++++++++++++ .../javascript/components/cart/store-cart.tsx | 33 +++++++++++-- .../components/store/store-product-item.tsx | 16 +++++-- .../src/javascript/components/store/store.tsx | 9 +++- app/frontend/templates/store/index.html | 1 + app/models/product_category.rb | 3 ++ app/services/cart/set_quantity_service.rb | 6 +-- config/locales/app.public.en.yml | 5 ++ config/locales/app.public.fr.yml | 7 ++- package.json | 1 + yarn.lock | 5 ++ 11 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 app/frontend/src/javascript/components/cart/cart-button.tsx diff --git a/app/frontend/src/javascript/components/cart/cart-button.tsx b/app/frontend/src/javascript/components/cart/cart-button.tsx new file mode 100644 index 000000000..3586ec263 --- /dev/null +++ b/app/frontend/src/javascript/components/cart/cart-button.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { react2angular } from 'react2angular'; +import { Loader } from '../base/loader'; +import { IApplication } from '../../models/application'; +import { Order } from '../../models/order'; +import { useCustomEventListener } from 'react-custom-events'; + +declare const Application: IApplication; + +/** + * This component shows my cart button + */ +const CartButton: React.FC = () => { + const { t } = useTranslation('public'); + const [cart, setCart] = useState(); + useCustomEventListener('CartUpdate', (data) => { + setCart(data); + }); + + /** + * Goto cart page + */ + const showCart = () => { + window.location.href = '/#!/cart'; + }; + + if (cart) { + return ( +
+ + {cart.order_items_attributes.length} +
{t('app.public.cart_button.my_cart')}
+
+ ); + } + return null; +}; + +const CartButtonWrapper: React.FC = () => { + return ( + + + + ); +}; + +Application.Components.component('cartButton', react2angular(CartButtonWrapper)); diff --git a/app/frontend/src/javascript/components/cart/store-cart.tsx b/app/frontend/src/javascript/components/cart/store-cart.tsx index 5ca920d38..1ac2c7a2b 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -20,7 +20,7 @@ interface StoreCartProps { const StoreCart: React.FC = ({ onError }) => { const { t } = useTranslation('public'); - const { loading, cart, setCart } = useCart(); + const { cart, setCart } = useCart(); /** * Remove the product from cart @@ -35,21 +35,46 @@ const StoreCart: React.FC = ({ onError }) => { }; }; + /** + * Change product quantity + */ + const changeProductQuantity = (item) => { + return (e: React.BaseSyntheticEvent) => { + CartAPI.setQuantity(cart, item.orderable_id, e.target.value).then(data => { + setCart(data); + }); + }; + }; + + /** + * Checkout cart + */ + const checkout = () => { + console.log('checkout .....'); + }; + return (
- {loading &&

loading

} {cart && cart.order_items_attributes.map(item => (
{item.orderable_name}
{FormatLib.price(item.amount)}
{item.quantity}
+
{FormatLib.price(item.quantity * item.amount)}
- {t('app.public.store_cart.remove_item')} +
))} - {cart &&

{cart.amount}

} + {cart &&

Totale: {FormatLib.price(cart.amount)}

} + + {t('app.public.store_cart.checkout')} +
); }; diff --git a/app/frontend/src/javascript/components/store/store-product-item.tsx b/app/frontend/src/javascript/components/store/store-product-item.tsx index fc7aa00f8..1c8a0e8fa 100644 --- a/app/frontend/src/javascript/components/store/store-product-item.tsx +++ b/app/frontend/src/javascript/components/store/store-product-item.tsx @@ -10,12 +10,13 @@ import CartAPI from '../../api/cart'; interface StoreProductItemProps { product: Product, cart: Order, + onSuccessAddProductToCart: (cart: Order) => void } /** * This component shows a product item in store */ -export const StoreProductItem: React.FC = ({ product, cart }) => { +export const StoreProductItem: React.FC = ({ product, cart, onSuccessAddProductToCart }) => { const { t } = useTranslation('public'); /** @@ -33,6 +34,9 @@ export const StoreProductItem: React.FC = ({ product, car * Return product's stock status */ const productStockStatus = (product: Product) => { + if (product.stock.external === 0) { + return {t('app.public.store_product_item.out_of_stock')}; + } if (product.low_stock_threshold && product.stock.external < product.low_stock_threshold) { return {t('app.public.store_product_item.limited_stock')}; } @@ -45,7 +49,7 @@ export const StoreProductItem: React.FC = ({ product, car const addProductToCart = (e: React.BaseSyntheticEvent) => { e.preventDefault(); e.stopPropagation(); - CartAPI.addItem(cart, product.id, 1); + CartAPI.addItem(cart, product.id, 1).then(onSuccessAddProductToCart); }; /** @@ -66,9 +70,11 @@ export const StoreProductItem: React.FC = ({ product, car
{FormatLib.price(product.amount)}
{productStockStatus(product)} - - {t('app.public.store_product_item.add')} - + {product.stock.external > 0 && + + {t('app.public.store_product_item.add')} + + } ); diff --git a/app/frontend/src/javascript/components/store/store.tsx b/app/frontend/src/javascript/components/store/store.tsx index 78339cf0a..e93789487 100644 --- a/app/frontend/src/javascript/components/store/store.tsx +++ b/app/frontend/src/javascript/components/store/store.tsx @@ -8,6 +8,7 @@ import { Product } from '../../models/product'; import ProductAPI from '../../api/product'; import { StoreProductItem } from './store-product-item'; import useCart from '../../hooks/use-cart'; +import { emitCustomEvent } from 'react-custom-events'; declare const Application: IApplication; @@ -21,7 +22,7 @@ interface StoreProps { const Store: React.FC = ({ onError }) => { const { t } = useTranslation('public'); - const { cart } = useCart(); + const { cart, setCart } = useCart(); const [products, setProducts] = useState>([]); @@ -33,6 +34,10 @@ const Store: React.FC = ({ onError }) => { }); }, []); + useEffect(() => { + emitCustomEvent('CartUpdate', cart); + }, [cart]); + return (
@@ -71,7 +76,7 @@ const Store: React.FC = ({ onError }) => {
{products.map((product) => ( - + ))}
diff --git a/app/frontend/templates/store/index.html b/app/frontend/templates/store/index.html index ef8bf3ff7..20d00a606 100644 --- a/app/frontend/templates/store/index.html +++ b/app/frontend/templates/store/index.html @@ -13,6 +13,7 @@
+
diff --git a/app/models/product_category.rb b/app/models/product_category.rb index fdc492f83..40af30a1a 100644 --- a/app/models/product_category.rb +++ b/app/models/product_category.rb @@ -3,6 +3,9 @@ # Category is a first-level filter, used to categorize Products. # It is mandatory to choose a Category when creating a Product. class ProductCategory < ApplicationRecord + extend FriendlyId + friendly_id :name, use: :slugged + validates :name, :slug, presence: true belongs_to :parent, class_name: 'ProductCategory' diff --git a/app/services/cart/set_quantity_service.rb b/app/services/cart/set_quantity_service.rb index d1ad0332c..9d0095c34 100644 --- a/app/services/cart/set_quantity_service.rb +++ b/app/services/cart/set_quantity_service.rb @@ -5,16 +5,16 @@ class Cart::SetQuantityService def call(order, orderable, quantity = nil) return order if quantity.to_i.zero? - raise Cart::OutStockError if quantity > orderable.stock['external'] + raise Cart::OutStockError if quantity.to_i > orderable.stock['external'] item = order.order_items.find_by(orderable: orderable) raise ActiveRecord::RecordNotFound if item.nil? - different_quantity = item.quantity - quantiy.to_i + different_quantity = quantity.to_i - item.quantity order.amount += (orderable.amount * different_quantity) ActiveRecord::Base.transaction do - item.update(quantity: quantity) + item.update(quantity: quantity.to_i) order.save end order.reload diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 1672b517c..ac9bf7ab1 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -381,11 +381,16 @@ en: store_product_item: available: "Available" limited_stock: "Limited stock" + out_of_stock: "Out of stock" add: "Add" store_product: unexpected_error_occurred: "An unexpected error occurred. Please try again later." cart: my_cart: "My Cart" + cart_button: + my_cart: "My Cart" + store_cart: + checkout: "Checkout" tour: conclusion: title: "Thank you for your attention" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 5325f6127..e971c8e5f 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -381,11 +381,16 @@ fr: store_product_item: available: "Disponible" limited_stock: "Stock limité" + out_of_stock: "Épuisé" add: "Ajouter" store_product: - unexpected_error_occurred: "An unexpected error occurred. Please try again later." + unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement." cart: my_cart: "Mon Panier" + cart_button: + my_cart: "Mon Panier" + store_cart: + checkout: "Valider mon panier" tour: conclusion: title: "Merci de votre attention" diff --git a/package.json b/package.json index 7144699d0..00e5295bf 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "rails-erb-loader": "^5.5.2", "react": "^17.0.2", "react-cool-onclickoutside": "^1.7.0", + "react-custom-events": "^1.1.1", "react-dom": "^17.0.2", "react-hook-form": "^7.30.0", "react-i18next": "^11.15.6", diff --git a/yarn.lock b/yarn.lock index 0add086b9..25384a7b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6588,6 +6588,11 @@ react-cool-onclickoutside@^1.7.0: resolved "https://registry.yarnpkg.com/react-cool-onclickoutside/-/react-cool-onclickoutside-1.7.0.tgz#abc844e14852220fe15f81d7ef44976d15cd9980" integrity sha512-HVZK2155Unee+enpoHKyYP2UdQK69thw90XAOUCjvJBcgRSgfRPgWWt/W1dYzoGp3+nleAa8SJxF1d4FMA4Qmw== +react-custom-events@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-custom-events/-/react-custom-events-1.1.1.tgz#792f126e897043a14b9f27a4c5ab7072ff235ceb" + integrity sha512-71iEu3zHsBn3uvF+Sq4Fu5imtRt+cLZO6nG2zqUhdqGVIpZIfeLcl6yieqPghrE+18KFrS5BaHD0NBPP/EZJNw== + react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"