mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
cart button
This commit is contained in:
parent
ab800a519f
commit
cfd21adb60
48
app/frontend/src/javascript/components/cart/cart-button.tsx
Normal file
48
app/frontend/src/javascript/components/cart/cart-button.tsx
Normal file
@ -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<Order>();
|
||||||
|
useCustomEventListener<Order>('CartUpdate', (data) => {
|
||||||
|
setCart(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goto cart page
|
||||||
|
*/
|
||||||
|
const showCart = () => {
|
||||||
|
window.location.href = '/#!/cart';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cart) {
|
||||||
|
return (
|
||||||
|
<div className="cart-button" onClick={showCart}>
|
||||||
|
<i className="fas fa-cart-arrow-down" />
|
||||||
|
<span>{cart.order_items_attributes.length}</span>
|
||||||
|
<div>{t('app.public.cart_button.my_cart')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CartButtonWrapper: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Loader>
|
||||||
|
<CartButton />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Application.Components.component('cartButton', react2angular(CartButtonWrapper));
|
@ -20,7 +20,7 @@ interface StoreCartProps {
|
|||||||
const StoreCart: React.FC<StoreCartProps> = ({ onError }) => {
|
const StoreCart: React.FC<StoreCartProps> = ({ onError }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
const { loading, cart, setCart } = useCart();
|
const { cart, setCart } = useCart();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the product from cart
|
* Remove the product from cart
|
||||||
@ -35,21 +35,46 @@ const StoreCart: React.FC<StoreCartProps> = ({ 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 (
|
return (
|
||||||
<div className="store-cart">
|
<div className="store-cart">
|
||||||
{loading && <p>loading</p>}
|
|
||||||
{cart && cart.order_items_attributes.map(item => (
|
{cart && cart.order_items_attributes.map(item => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<div>{item.orderable_name}</div>
|
<div>{item.orderable_name}</div>
|
||||||
<div>{FormatLib.price(item.amount)}</div>
|
<div>{FormatLib.price(item.amount)}</div>
|
||||||
<div>{item.quantity}</div>
|
<div>{item.quantity}</div>
|
||||||
|
<select value={item.quantity} onChange={changeProductQuantity(item)}>
|
||||||
|
{Array.from({ length: 100 }, (_, i) => i + 1).map(v => (
|
||||||
|
<option key={v} value={v}>{v}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
<div>{FormatLib.price(item.quantity * item.amount)}</div>
|
<div>{FormatLib.price(item.quantity * item.amount)}</div>
|
||||||
<FabButton className="delete-btn" onClick={removeProductFromCart(item)}>
|
<FabButton className="delete-btn" onClick={removeProductFromCart(item)}>
|
||||||
<i className="fa fa-trash" /> {t('app.public.store_cart.remove_item')}
|
<i className="fa fa-trash" />
|
||||||
</FabButton>
|
</FabButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{cart && <p>{cart.amount}</p>}
|
{cart && <p>Totale: {FormatLib.price(cart.amount)}</p>}
|
||||||
|
<FabButton className="checkout-btn" onClick={checkout}>
|
||||||
|
{t('app.public.store_cart.checkout')}
|
||||||
|
</FabButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,12 +10,13 @@ import CartAPI from '../../api/cart';
|
|||||||
interface StoreProductItemProps {
|
interface StoreProductItemProps {
|
||||||
product: Product,
|
product: Product,
|
||||||
cart: Order,
|
cart: Order,
|
||||||
|
onSuccessAddProductToCart: (cart: Order) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component shows a product item in store
|
* This component shows a product item in store
|
||||||
*/
|
*/
|
||||||
export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, cart }) => {
|
export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, cart, onSuccessAddProductToCart }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,6 +34,9 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
|
|||||||
* Return product's stock status
|
* Return product's stock status
|
||||||
*/
|
*/
|
||||||
const productStockStatus = (product: Product) => {
|
const productStockStatus = (product: Product) => {
|
||||||
|
if (product.stock.external === 0) {
|
||||||
|
return <span>{t('app.public.store_product_item.out_of_stock')}</span>;
|
||||||
|
}
|
||||||
if (product.low_stock_threshold && product.stock.external < product.low_stock_threshold) {
|
if (product.low_stock_threshold && product.stock.external < product.low_stock_threshold) {
|
||||||
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
||||||
}
|
}
|
||||||
@ -45,7 +49,7 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
|
|||||||
const addProductToCart = (e: React.BaseSyntheticEvent) => {
|
const addProductToCart = (e: React.BaseSyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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<StoreProductItemProps> = ({ product, car
|
|||||||
<div>{FormatLib.price(product.amount)}</div>
|
<div>{FormatLib.price(product.amount)}</div>
|
||||||
{productStockStatus(product)}
|
{productStockStatus(product)}
|
||||||
</span>
|
</span>
|
||||||
|
{product.stock.external > 0 &&
|
||||||
<FabButton className="edit-btn" onClick={addProductToCart}>
|
<FabButton className="edit-btn" onClick={addProductToCart}>
|
||||||
<i className="fas fa-cart-arrow-down" /> {t('app.public.store_product_item.add')}
|
<i className="fas fa-cart-arrow-down" /> {t('app.public.store_product_item.add')}
|
||||||
</FabButton>
|
</FabButton>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@ import { Product } from '../../models/product';
|
|||||||
import ProductAPI from '../../api/product';
|
import ProductAPI from '../../api/product';
|
||||||
import { StoreProductItem } from './store-product-item';
|
import { StoreProductItem } from './store-product-item';
|
||||||
import useCart from '../../hooks/use-cart';
|
import useCart from '../../hooks/use-cart';
|
||||||
|
import { emitCustomEvent } from 'react-custom-events';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ interface StoreProps {
|
|||||||
const Store: React.FC<StoreProps> = ({ onError }) => {
|
const Store: React.FC<StoreProps> = ({ onError }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
const { cart } = useCart();
|
const { cart, setCart } = useCart();
|
||||||
|
|
||||||
const [products, setProducts] = useState<Array<Product>>([]);
|
const [products, setProducts] = useState<Array<Product>>([]);
|
||||||
|
|
||||||
@ -33,6 +34,10 @@ const Store: React.FC<StoreProps> = ({ onError }) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
emitCustomEvent('CartUpdate', cart);
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="store">
|
<div className="store">
|
||||||
<div className='layout'>
|
<div className='layout'>
|
||||||
@ -71,7 +76,7 @@ const Store: React.FC<StoreProps> = ({ onError }) => {
|
|||||||
|
|
||||||
<div className="products">
|
<div className="products">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<StoreProductItem key={product.id} product={product} cart={cart}/>
|
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||||
<section class="heading-actions wrapper">
|
<section class="heading-actions wrapper">
|
||||||
|
<cart-button />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
# Category is a first-level filter, used to categorize Products.
|
# Category is a first-level filter, used to categorize Products.
|
||||||
# It is mandatory to choose a Category when creating a Product.
|
# It is mandatory to choose a Category when creating a Product.
|
||||||
class ProductCategory < ApplicationRecord
|
class ProductCategory < ApplicationRecord
|
||||||
|
extend FriendlyId
|
||||||
|
friendly_id :name, use: :slugged
|
||||||
|
|
||||||
validates :name, :slug, presence: true
|
validates :name, :slug, presence: true
|
||||||
|
|
||||||
belongs_to :parent, class_name: 'ProductCategory'
|
belongs_to :parent, class_name: 'ProductCategory'
|
||||||
|
@ -5,16 +5,16 @@ class Cart::SetQuantityService
|
|||||||
def call(order, orderable, quantity = nil)
|
def call(order, orderable, quantity = nil)
|
||||||
return order if quantity.to_i.zero?
|
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)
|
item = order.order_items.find_by(orderable: orderable)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if item.nil?
|
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)
|
order.amount += (orderable.amount * different_quantity)
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
item.update(quantity: quantity)
|
item.update(quantity: quantity.to_i)
|
||||||
order.save
|
order.save
|
||||||
end
|
end
|
||||||
order.reload
|
order.reload
|
||||||
|
@ -381,11 +381,16 @@ en:
|
|||||||
store_product_item:
|
store_product_item:
|
||||||
available: "Available"
|
available: "Available"
|
||||||
limited_stock: "Limited stock"
|
limited_stock: "Limited stock"
|
||||||
|
out_of_stock: "Out of stock"
|
||||||
add: "Add"
|
add: "Add"
|
||||||
store_product:
|
store_product:
|
||||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||||
cart:
|
cart:
|
||||||
my_cart: "My Cart"
|
my_cart: "My Cart"
|
||||||
|
cart_button:
|
||||||
|
my_cart: "My Cart"
|
||||||
|
store_cart:
|
||||||
|
checkout: "Checkout"
|
||||||
tour:
|
tour:
|
||||||
conclusion:
|
conclusion:
|
||||||
title: "Thank you for your attention"
|
title: "Thank you for your attention"
|
||||||
|
@ -381,11 +381,16 @@ fr:
|
|||||||
store_product_item:
|
store_product_item:
|
||||||
available: "Disponible"
|
available: "Disponible"
|
||||||
limited_stock: "Stock limité"
|
limited_stock: "Stock limité"
|
||||||
|
out_of_stock: "Épuisé"
|
||||||
add: "Ajouter"
|
add: "Ajouter"
|
||||||
store_product:
|
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:
|
cart:
|
||||||
my_cart: "Mon Panier"
|
my_cart: "Mon Panier"
|
||||||
|
cart_button:
|
||||||
|
my_cart: "Mon Panier"
|
||||||
|
store_cart:
|
||||||
|
checkout: "Valider mon panier"
|
||||||
tour:
|
tour:
|
||||||
conclusion:
|
conclusion:
|
||||||
title: "Merci de votre attention"
|
title: "Merci de votre attention"
|
||||||
|
@ -138,6 +138,7 @@
|
|||||||
"rails-erb-loader": "^5.5.2",
|
"rails-erb-loader": "^5.5.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-cool-onclickoutside": "^1.7.0",
|
"react-cool-onclickoutside": "^1.7.0",
|
||||||
|
"react-custom-events": "^1.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-i18next": "^11.15.6",
|
"react-i18next": "^11.15.6",
|
||||||
|
@ -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"
|
resolved "https://registry.yarnpkg.com/react-cool-onclickoutside/-/react-cool-onclickoutside-1.7.0.tgz#abc844e14852220fe15f81d7ef44976d15cd9980"
|
||||||
integrity sha512-HVZK2155Unee+enpoHKyYP2UdQK69thw90XAOUCjvJBcgRSgfRPgWWt/W1dYzoGp3+nleAa8SJxF1d4FMA4Qmw==
|
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:
|
react-dom@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user