1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

Style cart

This commit is contained in:
vincent 2022-08-29 16:32:35 +02:00
parent 5791e6076d
commit 78683a31b3
17 changed files with 421 additions and 126 deletions

View File

@ -25,16 +25,13 @@ const CartButton: React.FC = () => {
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;
return (
<div className="cart-button" onClick={showCart}>
<i className="fas fa-cart-arrow-down" />
<span>{cart?.order_items_attributes?.length}</span>
<p>{t('app.public.cart_button.my_cart')}</p>
</div>
);
};
const CartButtonWrapper: React.FC = () => {

View File

@ -15,10 +15,12 @@ import { MemberSelect } from '../user/member-select';
import { CouponInput } from '../coupon/coupon-input';
import { Coupon } from '../../models/coupon';
import { computePriceWithCoupon } from '../../lib/coupon';
import noImage from '../../../../images/no_image.png';
declare const Application: IApplication;
interface StoreCartProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
userLogin: () => void,
currentUser?: User
@ -27,10 +29,11 @@ interface StoreCartProps {
/**
* This component shows user's cart
*/
const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser, userLogin }) => {
const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser, userLogin }) => {
const { t } = useTranslation('public');
const { cart, setCart } = useCart(currentUser);
console.log('cart: ', cart);
const [paymentModal, setPaymentModal] = useState<boolean>(false);
/**
@ -76,14 +79,15 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser, userLogin }
};
/**
* Open/closes the payment modal
* Handle payment
*/
const handlePaymentSuccess = (data: Order): void => {
if (data.payment_state === 'paid') {
setPaymentModal(false);
window.location.href = '/#!/store';
onSuccess(t('app.public.store_cart.checkout_success'));
} else {
onError('Erreur inconnue after payment, please conntact admin');
onError(t('app.public.store_cart.checkout_error'));
}
};
@ -117,35 +121,90 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser, userLogin }
}
};
/**
* Get the offered item total
*/
const offeredAmount = (): number => {
return cart.order_items_attributes
.filter(i => i.is_offered)
.map(i => i.amount)
.reduce((acc, curr) => acc + curr, 0);
};
return (
<div className="store-cart">
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
{cart && cart.order_items_attributes.map(item => (
<div key={item.id}>
<div>{item.orderable_name}</div>
<div>{FormatLib.price(item.amount)}</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>
<FabButton className="delete-btn" onClick={removeProductFromCart(item)}>
<i className="fa fa-trash" />
</FabButton>
<div className='store-cart'>
<div className="store-cart-list">
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
{cart && cart.order_items_attributes.map(item => (
<article key={item.id} className='store-cart-list-item'>
<div className='picture'>
<img alt=''src={noImage} />
</div>
<div className="ref">
<span>ref: </span>
<p>{item.orderable_name}</p>
</div>
<div className="actions">
<div className='price'>
<p>{FormatLib.price(item.amount)}</p>
<span>/ {t('app.public.store_cart.unit')}</span>
</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 className='total'>
<span>{t('app.public.store_cart.total')}</span>
<p>{FormatLib.price(item.quantity * item.amount)}</p>
</div>
<FabButton className="main-action-btn" onClick={removeProductFromCart(item)}>
<i className="fa fa-trash" />
</FabButton>
</div>
</article>
))}
</div>
<div className="group">
<div className='store-cart-info'>
<h3>{t('app.public.store_cart.pickup')}</h3>
<p>[TODO: texte venant des paramètres de la boutique]</p>
</div>
))}
{cart && !cartIsEmpty() && cart.user && <CouponInput user={cart.user as User} amount={cart.total} onChange={applyCoupon} />}
{cart && !cartIsEmpty() && <p>Total produits: {FormatLib.price(cart.total)}</p>}
{cart && !cartIsEmpty() && cart.coupon && computePriceWithCoupon(cart.total, cart.coupon) !== cart.total && <p>Coupon réduction: {FormatLib.price(-(cart.total - computePriceWithCoupon(cart.total, cart.coupon)))}</p>}
{cart && !cartIsEmpty() && <p>Total panier: {FormatLib.price(computePriceWithCoupon(cart.total, cart.coupon))}</p>}
{cart && !cartIsEmpty() && isPrivileged() && <MemberSelect onSelected={handleChangeMember} />}
{cart && !cartIsEmpty() &&
<FabButton className="checkout-btn" onClick={checkout}>
{t('app.public.store_cart.checkout')}
</FabButton>
}
{cart && !cartIsEmpty() && cart.user &&
<div className='store-cart-coupon'>
<CouponInput user={cart.user as User} amount={cart.total} onChange={applyCoupon} />
</div>
}
</div>
<aside>
{cart && !cartIsEmpty() && isPrivileged() &&
<div> <MemberSelect onSelected={handleChangeMember} /></div>
}
{cart && !cartIsEmpty() && <>
<div className="checkout">
<h3>{t('app.public.store_cart.checkout_header')}</h3>
<span>{t('app.public.store_cart.checkout_products_COUNT', { COUNT: cart?.order_items_attributes.length })}</span>
<div className="list">
<p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(cart.total)}</span></p>
{offeredAmount() > 0 &&
<p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(offeredAmount())}</span></p>
}
{cart.coupon && computePriceWithCoupon(cart.total, cart.coupon) !== cart.total &&
<p>{t('app.public.store_cart.checkout_coupon')} <span>{FormatLib.price(-(cart.total - computePriceWithCoupon(cart.total, cart.coupon)))}</span></p>
}
</div>
<p className='total'>{t('app.public.store_cart.checkout_total')} <span>{FormatLib.price(computePriceWithCoupon(cart.total, cart.coupon))}</span></p>
</div>
<FabButton className='checkout-btn' onClick={checkout} disabled={!cart.user || cart.order_items_attributes.length === 0}>
{t('app.public.store_cart.checkout')}
</FabButton>
</>}
</aside>
{cart && !cartIsEmpty() && cart.user && <div>
<PaymentModal isOpen={paymentModal}
toggleModal={togglePaymentModal}
@ -169,4 +228,4 @@ const StoreCartWrapper: React.FC<StoreCartProps> = (props) => {
);
};
Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onError', 'currentUser', 'userLogin']));
Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onSuccess', 'onError', 'currentUser', 'userLogin']));

View File

@ -51,7 +51,18 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
useEffect(() => {
ProductCategoryAPI.index().then(data => {
setProductCategories(buildSelectOptions(data));
// Map product categories by position
const sortedCategories = data
.filter(c => !c.parent_id)
.sort((a, b) => a.position - b.position);
const childrenCategories = data
.filter(c => typeof c.parent_id === 'number')
.sort((a, b) => b.position - a.position);
childrenCategories.forEach(c => {
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
sortedCategories.splice(parentIndex + 1, 0, c);
});
setProductCategories(buildSelectOptions(sortedCategories));
}).catch(onError);
MachineAPI.index({ disabled: false }).then(data => {
setMachines(buildChecklistOptions(data));

View File

@ -1,5 +1,3 @@
// TODO: Remove next eslint-disable
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useEffect } from 'react';
import { useImmer } from 'use-immer';
import { useTranslation } from 'react-i18next';
@ -35,6 +33,7 @@ interface ProductsProps {
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [products, setProducts] = useState<Array<Product>>([]);
const [filteredProductsList, setFilteredProductList] = useImmer<Array<Product>>([]);
const [features, setFeatures] = useImmer<Filters>(initFilters);

View File

@ -103,6 +103,13 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
setToCartCount(Number(evt.target.value));
};
/**
* Add product to cart
*/
const addToCart = () => {
console.log('Add', toCartCount, 'to cart');
};
if (product) {
return (
<div className={`store-product ${statusColor(product)}`}>
@ -110,14 +117,14 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
<h2 className='name'>{product.name}</h2>
<div className='gallery'>
<div className='main'>
<div className='aspect-ratio'>
<div className='picture'>
<img src={productImageUrl(showImage)} alt='' />
</div>
</div>
{product.product_images_attributes.length > 1 &&
<div className='thumbnails'>
{product.product_images_attributes.map(i => (
<div key={i.id} className={`aspect-ratio ${i.id === showImage ? 'is-active' : ''}`}>
<div key={i.id} className={`picture ${i.id === showImage ? 'is-active' : ''}`}>
<img alt='' onClick={() => setShowImage(i.id)} src={i.attachment_url} />
</div>
))}
@ -167,7 +174,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
value={toCartCount}
onChange={evt => typeCount(evt)} />
<FabButton onClick={() => setCount('add')} icon={<Plus size={16} />} className="plus" />
<FabButton onClick={() => console.log('Add', toCartCount, 'to cart')} icon={<i className="fas fa-cart-arrow-down" />}
<FabButton onClick={() => addToCart()} icon={<i className="fas fa-cart-arrow-down" />}
className="main-action-btn">
{t('app.public.store_product_item.add_to_cart')}
</FabButton>

View File

@ -69,12 +69,6 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
emitCustomEvent('CartUpdate', cart);
}, [cart]);
useEffect(() => {
if (currentUser) {
reloadCart();
}
}, [currentUser]);
/**
* Create categories tree (parent/children)
*/

View File

@ -30,6 +30,8 @@
@import "modules/base/fab-text-editor";
@import "modules/base/labelled-input";
@import "modules/calendar/calendar";
@import "modules/cart/cart-button";
@import "modules/cart/store-cart";
@import "modules/dashboard/reservations/credits-panel";
@import "modules/dashboard/reservations/reservations-dashboard";
@import "modules/dashboard/reservations/reservations-panel";
@ -42,6 +44,7 @@
@import "modules/form/form-file-upload";
@import "modules/form/form-image-upload";
@import "modules/group/change-group";
@import "modules/layout/header-page";
@import "modules/machines/machine-card";
@import "modules/machines/machines-filters";
@import "modules/machines/machines-list";

View File

@ -0,0 +1,41 @@
.cart-button {
position: relative;
width: 100%;
height: 100%;
padding: 0.8rem 0.6rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
background-color: var(--secondary);
&:hover {
cursor: pointer;
}
span {
position: absolute;
top: 1rem;
right: 1rem;
min-width: 2rem;
height: 2rem;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--secondary-text-color);
border-radius: 10rem;
color: var(--secondary);
@include text-sm(600);
}
i {
margin-bottom: 0.8rem;
font-size: 2.6rem;
columns: var(--secondary-text-color);
}
p {
margin: 0;
@include text-sm;
text-align: center;
color: var(--secondary-text-color);
}
}

View File

@ -0,0 +1,160 @@
.store-cart {
width: 100%;
max-width: 1600px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
grid-template-rows: minmax(0, min-content);
gap: 3.2rem;
align-items: flex-start;
&-list {
grid-area: 1 / 1 / 2 / 10;
display: grid;
gap: 1.6rem;
&-item {
padding: 0.8rem;
display: grid;
grid-auto-flow: column;
grid-template-columns: min-content 1fr;
gap: 1.6rem;
justify-content: space-between;
align-items: center;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
.picture {
width: 10rem !important;
@include imageRatio(76%);
border-radius: var(--border-radius);
}
.ref {
display: flex;
flex-direction: column;
span {
@include text-sm;
color: var(--gray-hard-lightest);
text-transform: uppercase;
}
p {
max-width: 60ch;
margin: 0;
@include text-base(600);
}
}
.actions {
align-self: stretch;
padding: 0.8rem;
display: grid;
grid-auto-flow: column;
justify-content: space-between;
align-items: center;
gap: 2.4rem;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
}
.price,
.total {
min-width: 10rem;
p {
margin: 0;
display: flex;
@include title-base;
}
span { @include text-sm; }
}
.total {
span {
@include text-sm;
color: var(--main);
text-transform: uppercase;
}
}
}
}
.group {
grid-area: 2 / 1 / 3 / 10;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2.4rem;
}
&-info,
&-coupon {
padding: 2.4rem;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
h3, label {
margin: 0 0 1.6rem;
@include text-base(500);
color: var(--gray-hard-darkest) !important;
}
.fab-input .input-wrapper {
width: 100%;
.fab-input--input {
border-radius: var(--border-radius);
}
}
}
&-info {
p { @include text-sm; }
}
&-coupon {
}
aside {
grid-area: 1 / 10 / 3 / 13;
& > div {
margin-bottom: 3.2rem;
padding: 1.6rem;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
h3,
.member-select-title {
margin: 0 0 2.4rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid var(--gray-hard);
@include title-base;
color: var(--gray-hard-dark) !important;
}
}
.checkout {
.list {
margin: 0.8rem 0 2.4rem;
padding: 2.4rem 0;
border-top: 1px solid var(--main);
border-bottom: 1px solid var(--main);
p {
display: flex;
justify-content: space-between;
align-items: center;
span { @include title-base; }
}
.gift { color: var(--information); }
}
.total {
display: flex;
justify-content: space-between;
align-items: flex-start;
@include text-base(600);
span { @include title-lg; }
}
&-btn {
width: 100%;
height: auto;
padding: 1.6rem 0.8rem;
background-color: var(--information);
border: none;
color: var(--gray-soft-lightest);
justify-content: center;
text-transform: uppercase;
&:hover {
opacity: 0.75;
}
}
}
}
}

View File

@ -0,0 +1,37 @@
.header-page {
width: 100%;
min-height: 9rem;
display: grid;
grid-template-columns: min-content 1fr min-content;
background-color: var(--gray-soft-lightest);
border-bottom: 1px solid var(--gray-soft-dark);
.back {
width: 9rem;
border-right: 1px solid var(--gray-soft-dark);
a {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: var(--gray-hard-darkest) !important;
&:hover {
cursor: pointer;
background-color: var(--secondary);
}
}
}
.center {
padding: 3.2rem;
h1 {
margin: 0;
}
}
.right {
min-width: 9rem;
border-left: 1px solid var(--gray-soft-dark);
}
}

View File

@ -26,19 +26,8 @@
.picture {
grid-area: image;
position: relative;
width: 100%;
height: 0;
padding-bottom: 50%;
@include imageRatio(50%);
border-radius: var(--border-radius);
overflow: hidden;
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.name {
margin: 2.4rem 0 1.6rem;

View File

@ -57,19 +57,11 @@
}
.gallery {
grid-area: 3 / 1 / 4 / 4;
.aspect-ratio {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
overflow: hidden;
.picture{
@include imageRatio;
border-radius: var(--border-radius-sm);
border: 1px solid var(--gray-soft-darkest);
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
cursor: pointer;
}
@ -184,6 +176,7 @@
display: grid;
grid-template-areas: "minus input plus"
"btn btn btn";
grid-template-columns: repeat(3, minmax(0, min-content));
gap: 1.6rem;
border-top: 1px solid var(--gray-soft-dark);
.minus {

View File

@ -2,4 +2,19 @@
--border-radius: 8px;
--border-radius-sm: 4px;
--shadow: 0 0 10px rgba(39, 32, 32, 0.25);
}
@mixin imageRatio($ratio: 100%) {
position: relative;
width: 100%;
height: 0;
padding-bottom: $ratio;
overflow: hidden;
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}

View File

@ -1,18 +1,12 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'app.public.cart.my_cart' }}</h1>
</section>
</div>
<div class="header-page">
<div class="back">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</div>
</section>
<div class="center">
<h1 translate>{{ 'app.public.cart.my_cart' }}</h1>
</div>
</div>
<section class="m-lg">
<store-cart current-user="currentUser" user-login="userLogin" on-error="onError" on-success="onSuccess" />

View File

@ -1,24 +1,17 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
</section>
</div>
<div class="header-page">
<div class="back">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</div>
</section>
<div class="center">
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
</div>
<section>
<div class="right">
<cart-button />
</div>
</div>
<section class="m-lg">
<store-product product-slug="productSlug" on-error="onError" on-success="onSuccess" />
</section>

View File

@ -1,24 +1,16 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<cart-button />
</section>
</div>
<div class="header-page">
<div class="back">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</div>
</section>
<div class="center">
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
</div>
<div class="right">
<cart-button />
</div>
</div>
<section class="m-lg">
<store current-user="currentUser" on-error="onError" on-success="onSuccess" />

View File

@ -416,6 +416,17 @@ en:
store_cart:
checkout: "Checkout"
cart_is_empty: "Your cart is empty"
pickup: "Pickup your products"
unit: "Unit"
total: "Total"
checkout_header: "Total amount for your cart"
checkout_products_COUNT: "Your cart contains {COUNT} {COUNT, plural, =1{product} other{products}}"
checkout_products_total: "Product total"
checkout_gift_total: "Discount total"
checkout_coupon: "Coupon"
checkout_total: "Total amount"
checkout_error: "An unexpected error occurred. Please contact the administrator."
checkout_success: "Purchase confirmed. Thanks!"
member_select:
select_a_member: "Select a member"
start_typing: "Start typing..."