mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
Client side product list + product view
This commit is contained in:
parent
29993b0ec9
commit
e0dc008d4c
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
@ -352,7 +352,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
</FabAlert>
|
||||
<FormRichText control={control}
|
||||
paragraphTools={true}
|
||||
limit={1000}
|
||||
limit={6000}
|
||||
id="description" />
|
||||
</div>
|
||||
|
||||
|
@ -51,7 +51,7 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
if (product.stock.external === 0 && product.stock.internal === 0) {
|
||||
return 'out-of-stock';
|
||||
}
|
||||
if (product.low_stock_alert) {
|
||||
if (product.low_stock_threshold && (product.stock.external < product.low_stock_threshold || product.stock.internal < product.low_stock_threshold)) {
|
||||
return 'low';
|
||||
}
|
||||
};
|
||||
@ -70,11 +70,11 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
: t('app.admin.store.product_item.hidden')
|
||||
}
|
||||
</span>
|
||||
<div className='stock'>
|
||||
<div className={`stock ${product.stock.internal < product.low_stock_threshold ? 'low' : ''}`}>
|
||||
<span>{t('app.admin.store.product_item.stock.internal')}</span>
|
||||
<p>{product.stock.internal}</p>
|
||||
</div>
|
||||
<div className='stock'>
|
||||
<div className={`stock ${product.stock.external < product.low_stock_threshold ? 'low' : ''}`}>
|
||||
<span>{t('app.admin.store.product_item.stock.external')}</span>
|
||||
<p>{product.stock.external}</p>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import { Product } from '../../models/product';
|
||||
import { Order } from '../../models/order';
|
||||
import FormatLib from '../../lib/format';
|
||||
import CartAPI from '../../api/cart';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
|
||||
interface StoreProductItemProps {
|
||||
product: Product,
|
||||
@ -20,27 +21,14 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Return main image of Product, if the product has not any image, show default image
|
||||
* Return main image of Product, if the product has no image, show default image
|
||||
*/
|
||||
const productImageUrl = (product: Product) => {
|
||||
const productImage = _.find(product.product_images_attributes, { is_main: true });
|
||||
if (productImage) {
|
||||
return productImage.attachment_url;
|
||||
}
|
||||
return 'https://via.placeholder.com/300';
|
||||
};
|
||||
|
||||
/**
|
||||
* Return product's stock status
|
||||
*/
|
||||
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) {
|
||||
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
||||
}
|
||||
return <span>{t('app.public.store_product_item.available')}</span>;
|
||||
return noImage;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -59,23 +47,51 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
|
||||
window.location.href = `/#!/store/p/${product.slug}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns CSS class from stock status
|
||||
*/
|
||||
const statusColor = (product: Product) => {
|
||||
if (product.stock.external === 0 && product.stock.internal === 0) {
|
||||
return 'out-of-stock';
|
||||
}
|
||||
if (product.low_stock_alert) {
|
||||
return 'low';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return product's stock status
|
||||
*/
|
||||
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) {
|
||||
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
||||
}
|
||||
return <span>{t('app.public.store_product_item.available')}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="store-product-item" onClick={() => showProduct(product)}>
|
||||
<div className='itemInfo-image'>
|
||||
<img src={productImageUrl(product)} alt='' className='itemInfo-thumbnail' />
|
||||
<div className={`store-product-item ${statusColor(product)}`} onClick={() => showProduct(product)}>
|
||||
<div className="picture">
|
||||
<img src={productImageUrl(product)} alt='' />
|
||||
</div>
|
||||
<p className="itemInfo-name">{product.name}</p>
|
||||
<div className=''>
|
||||
<span>
|
||||
<div>{FormatLib.price(product.amount)}</div>
|
||||
{productStockStatus(product)}
|
||||
</span>
|
||||
{product.stock.external > 0 &&
|
||||
<FabButton className="edit-btn" onClick={addProductToCart}>
|
||||
<i className="fas fa-cart-arrow-down" /> {t('app.public.store_product_item.add')}
|
||||
</FabButton>
|
||||
}
|
||||
<p className="name">{product.name}</p>
|
||||
{product.amount &&
|
||||
<div className='price'>
|
||||
<p>{FormatLib.price(product.amount)}</p>
|
||||
<span>/ {t('app.public.store_product_item.unit')}</span>
|
||||
</div>
|
||||
}
|
||||
<div className="stock">
|
||||
{productStockStatus(product)}
|
||||
</div>
|
||||
{product.stock.external > 0 &&
|
||||
<FabButton icon={<i className="fas fa-cart-arrow-down" />} className="main-action-btn" onClick={addProductToCart}>
|
||||
{t('app.public.store_product_item.add')}
|
||||
</FabButton>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
/* eslint-disable fabmanager/scoped-translation */
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import _ from 'lodash';
|
||||
import { Product } from '../../models/product';
|
||||
import ProductAPI from '../../api/product';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FilePdf, Minus, Plus } from 'phosphor-react';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -21,32 +26,155 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const [product, setProduct] = useState<Product>();
|
||||
console.log('product: ', product);
|
||||
const [showImage, setShowImage] = useState<number>(null);
|
||||
const [toCartCount, setToCartCount] = useState<number>(0);
|
||||
const [displayToggle, setDisplayToggle] = useState<boolean>(false);
|
||||
const [collapseDescription, setCollapseDescription] = useState<boolean>(true);
|
||||
const descContainer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
ProductAPI.get(productSlug).then(data => {
|
||||
setProduct(data);
|
||||
const productImage = _.find(data.product_images_attributes, { is_main: true });
|
||||
setShowImage(productImage.id);
|
||||
setToCartCount(data.quantity_min ? data.quantity_min : 1);
|
||||
setDisplayToggle(descContainer.current.offsetHeight < descContainer.current.scrollHeight);
|
||||
}).catch(() => {
|
||||
onError(t('app.public.store_product.unexpected_error_occurred'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Return main image of Product, if the product has not any image, show default image
|
||||
* Return main image of Product, if the product has no image, show default image
|
||||
*/
|
||||
const productImageUrl = (product: Product) => {
|
||||
const productImage = _.find(product.product_images_attributes, { is_main: true });
|
||||
const productImageUrl = (id: number) => {
|
||||
const productImage = _.find(product.product_images_attributes, { id });
|
||||
if (productImage) {
|
||||
return productImage.attachment_url;
|
||||
}
|
||||
return 'https://via.placeholder.com/300';
|
||||
return noImage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns CSS class from stock status
|
||||
*/
|
||||
const statusColor = (product: Product) => {
|
||||
if (product.stock.external === 0 && product.stock.internal === 0) {
|
||||
return 'out-of-stock';
|
||||
}
|
||||
if (product.low_stock_alert) {
|
||||
return 'low';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return product's stock status
|
||||
*/
|
||||
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) {
|
||||
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
||||
}
|
||||
return <span>{t('app.public.store_product_item.available')}</span>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update product count
|
||||
*/
|
||||
const setCount = (type: 'add' | 'remove') => {
|
||||
switch (type) {
|
||||
case 'add':
|
||||
setToCartCount(toCartCount + 1);
|
||||
break;
|
||||
case 'remove':
|
||||
if (toCartCount > product.quantity_min) {
|
||||
setToCartCount(toCartCount - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Update product count
|
||||
*/
|
||||
const typeCount = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
evt.preventDefault();
|
||||
setToCartCount(Number(evt.target.value));
|
||||
};
|
||||
|
||||
if (product) {
|
||||
return (
|
||||
<div className="store-product">
|
||||
<img src={productImageUrl(product)} alt='' className='itemInfo-thumbnail' />
|
||||
<p className="itemInfo-name">{product.name}</p>
|
||||
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
||||
<div className={`store-product ${statusColor(product)}`}>
|
||||
<span className='ref'>ref: {product.sku}</span>
|
||||
<h2 className='name'>{product.name}</h2>
|
||||
<div className='gallery'>
|
||||
<div className='main'>
|
||||
<div className='aspect-ratio'>
|
||||
<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' : ''}`}>
|
||||
<img alt='' onClick={() => setShowImage(i.id)} src={i.attachment_url} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='description'>
|
||||
<div ref={descContainer} dangerouslySetInnerHTML={{ __html: product.description }} className='description-text' style={{ maxHeight: collapseDescription ? '35rem' : '1000rem' }} />
|
||||
{displayToggle &&
|
||||
<button onClick={() => setCollapseDescription(!collapseDescription)} className='description-toggle'>
|
||||
{collapseDescription
|
||||
? <span>{t('app.public.store_product.show_more')}</span>
|
||||
: <span>{t('app.public.store_product.show_less')}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
{product.product_files_attributes.length > 0 &&
|
||||
<div className='description-document'>
|
||||
<p>{t('app.public.store_product.documentation')}</p>
|
||||
<div className='list'>
|
||||
{product.product_files_attributes.map(f =>
|
||||
<a key={f.id} href={f.attachment_url}
|
||||
target='_blank'
|
||||
className='fab-button'
|
||||
rel='noreferrer'>
|
||||
<FilePdf size={24} />
|
||||
<span>{f.attachment_name}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div className="stock">
|
||||
{productStockStatus(product)}
|
||||
</div>
|
||||
<div className='price'>
|
||||
<p>{FormatLib.price(product.amount)} <sup>TTC</sup></p>
|
||||
<span>/ {t('app.public.store_product_item.unit')}</span>
|
||||
</div>
|
||||
{product.stock.external > 0 &&
|
||||
<div className='to-cart'>
|
||||
<FabButton onClick={() => setCount('remove')} icon={<Minus size={16} />} className="minus" />
|
||||
<input type="number"
|
||||
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" />}
|
||||
className="main-action-btn">
|
||||
{t('app.public.store_product_item.add')}
|
||||
</FabButton>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -49,7 +49,18 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
|
||||
});
|
||||
|
||||
ProductCategoryAPI.index().then(data => {
|
||||
setProductCategories(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(sortedCategories);
|
||||
}).catch(() => {
|
||||
onError(t('app.public.store.unexpected_error_occurred'));
|
||||
});
|
||||
|
@ -40,7 +40,11 @@
|
||||
background-color: var(--main);
|
||||
color: var(--gray-soft-lightest);
|
||||
border: none;
|
||||
&:hover { opacity: 0.75; }
|
||||
&:hover {
|
||||
background-color: var(--main);
|
||||
color: var(--gray-soft-lightest);
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin header {
|
||||
|
@ -1,9 +1,85 @@
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 3.2rem;
|
||||
|
||||
.store-product-item {
|
||||
color: tomato;
|
||||
--status-color: var(--success);
|
||||
&.low { --status-color: var(--alert-light); }
|
||||
&.out-of-stock {
|
||||
--status-color: var(--alert);
|
||||
background-color: var(--gray-soft-light);
|
||||
border: none;
|
||||
}
|
||||
|
||||
padding: 1.6rem 2.4rem;
|
||||
display: grid;
|
||||
grid-template-areas: "image image"
|
||||
"name name"
|
||||
"price btn"
|
||||
"stock btn";
|
||||
grid-template-columns: auto min-content;
|
||||
grid-template-rows: min-content auto min-content min-content;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
.picture {
|
||||
grid-area: image;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 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;
|
||||
grid-area: name;
|
||||
align-self: flex-start;
|
||||
@include text-base(600);
|
||||
}
|
||||
.price {
|
||||
grid-area: price;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
p {
|
||||
margin: 0;
|
||||
@include title-base;
|
||||
}
|
||||
span {
|
||||
margin-left: 0.8rem;
|
||||
@include text-sm;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.stock {
|
||||
grid-area: stock;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include text-sm;
|
||||
color: var(--status-color);
|
||||
&::before {
|
||||
content: "";
|
||||
margin-right: 0.8rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: var(--status-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
button {
|
||||
grid-area: btn;
|
||||
align-self: flex-end;
|
||||
margin-left: 1rem;
|
||||
i { margin-right: 0.8rem; }
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,10 @@
|
||||
.product-item {
|
||||
--status-color: var(--gray-hard-darkest);
|
||||
&.low { --status-color: var(--alert-light); }
|
||||
&.out-of-stock { --status-color: var(--alert); }
|
||||
&.out-of-stock {
|
||||
--status-color: var(--alert);
|
||||
.stock { color: var(--alert) !important; }
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@ -74,8 +77,9 @@
|
||||
.stock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--status-color);
|
||||
color: var(--gray-hard-darkest);
|
||||
span { @include text-xs; }
|
||||
&.low { color: var(--alert-light); }
|
||||
}
|
||||
.price {
|
||||
justify-self: flex-end;
|
||||
|
@ -1,52 +1,180 @@
|
||||
.store {
|
||||
.store,
|
||||
.store-product {
|
||||
max-width: 1600px;
|
||||
@include grid-col(12);
|
||||
gap: 3.2rem;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 6rem;
|
||||
|
||||
//&-product-item {
|
||||
// padding: 1rem 1.8rem;
|
||||
// border: 1px solid var(--gray-soft-dark);
|
||||
// border-radius: var(--border-radius);
|
||||
// background-color: var(--gray-soft-lightest);
|
||||
|
||||
// margin-right: 1.6rem;
|
||||
|
||||
// .itemInfo-image {
|
||||
// align-items: center;
|
||||
|
||||
// img {
|
||||
// width: 19.8rem;
|
||||
// height: 14.8rem;
|
||||
// object-fit: cover;
|
||||
// border-radius: var(--border-radius);
|
||||
// background-color: var(--gray-soft);
|
||||
// }
|
||||
// }
|
||||
// .itemInfo-name {
|
||||
// margin: 1rem 0;
|
||||
// @include text-base;
|
||||
// font-weight: 600;
|
||||
// color: var(--gray-hard-darkest);
|
||||
// }
|
||||
|
||||
// .actions {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// .manage {
|
||||
// overflow: hidden;
|
||||
// display: flex;
|
||||
// border-radius: var(--border-radius-sm);
|
||||
// button {
|
||||
// @include btn;
|
||||
// border-radius: 0;
|
||||
// color: var(--gray-soft-lightest);
|
||||
// &:hover { opacity: 0.75; }
|
||||
// }
|
||||
// .edit-btn {background: var(--gray-hard-darkest) }
|
||||
// .delete-btn {background: var(--error) }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.store-product {
|
||||
--status-color: var(--success);
|
||||
&.low { --status-color: var(--alert-light); }
|
||||
&.out-of-stock { --status-color: var(--alert); }
|
||||
|
||||
padding-top: 4rem;
|
||||
gap: 0 3.2rem;
|
||||
align-items: flex-start;
|
||||
|
||||
.ref {
|
||||
grid-area: 1 / 1 / 2 / 9;
|
||||
@include text-sm;
|
||||
color: var(--gray-hard-lightest);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.name {
|
||||
grid-area: 2 / 1 / 3 / 9;
|
||||
margin: 0.8rem 0 3.2rem;
|
||||
@include title-lg;
|
||||
color: var(--gray-hard-darkest) !important;
|
||||
}
|
||||
.gallery {
|
||||
grid-area: 3 / 1 / 4 / 4;
|
||||
.aspect-ratio {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.thumbnails {
|
||||
margin-top: 1.6rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.8rem;
|
||||
.is-active {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 2px var(--gray-hard-darkest);
|
||||
}
|
||||
}
|
||||
}
|
||||
.description {
|
||||
grid-area: 3 / 4 / 4 / 9;
|
||||
&-text {
|
||||
padding-bottom: 4rem;
|
||||
overflow: hidden;
|
||||
@include editor;
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
h3 {
|
||||
@include text-base(600);
|
||||
}
|
||||
p {
|
||||
@include text-sm;
|
||||
color: var(--gray-hard-lightest);
|
||||
}
|
||||
}
|
||||
&-toggle {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
background: linear-gradient(0deg, white 0%, transparent 100%);
|
||||
border: none;
|
||||
transform: translateY(-4rem);
|
||||
&::before {
|
||||
position: absolute;
|
||||
bottom: 1.2rem;
|
||||
left: 0;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--information);
|
||||
z-index: -1;
|
||||
}
|
||||
span {
|
||||
padding: 0 1.6rem;
|
||||
color: var(--information);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
}
|
||||
}
|
||||
&-document {
|
||||
padding: 2.4rem;
|
||||
background-color: var(--gray-soft-light);
|
||||
p { @include text-sm(500); }
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem 1.6rem;
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg { margin-right: 0.8rem; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
aside {
|
||||
grid-area: 1 / -4 / 4 / -1;
|
||||
position: sticky;
|
||||
top: 4rem;
|
||||
padding: 4rem;
|
||||
background-color: var(--gray-soft-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
.stock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include text-sm;
|
||||
color: var(--status-color);
|
||||
&::before {
|
||||
content: "";
|
||||
margin-right: 0.8rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: var(--status-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.price {
|
||||
p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
@include title-xl;
|
||||
sup {
|
||||
margin: 0.8rem 0 0 0.8rem;
|
||||
@include title-sm;
|
||||
}
|
||||
}
|
||||
span {
|
||||
@include text-sm;
|
||||
}
|
||||
}
|
||||
.to-cart {
|
||||
margin-top: 1.6rem;
|
||||
padding-top: 3.2rem;
|
||||
display: grid;
|
||||
grid-template-areas: "minus input plus"
|
||||
"btn btn btn";
|
||||
gap: 1.6rem;
|
||||
border-top: 1px solid var(--gray-soft-dark);
|
||||
.minus {
|
||||
grid-area: minus;
|
||||
color: var(--gray-hard-darkest);
|
||||
}
|
||||
.plus {
|
||||
grid-area: plus;
|
||||
color: var(--gray-hard-darkest);
|
||||
}
|
||||
input {
|
||||
grid-area: input;
|
||||
text-align: center;
|
||||
}
|
||||
.main-action-btn {
|
||||
grid-area: btn;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@
|
||||
h3 {
|
||||
@include text-lg(600);
|
||||
margin: 0 0 1rem;
|
||||
color: var(--gray-hard-darkest);
|
||||
color: var(--gray-hard-darkest) !important;
|
||||
}
|
||||
ul {
|
||||
padding-inline-start: 2.2rem;
|
||||
|
@ -19,6 +19,6 @@
|
||||
</section>
|
||||
|
||||
|
||||
<section class="m-lg">
|
||||
<section>
|
||||
<store-product product-slug="productSlug" on-error="onError" on-success="onSuccess" />
|
||||
</section>
|
||||
|
@ -396,6 +396,11 @@ en:
|
||||
price_high: "Price: high to low"
|
||||
store_product:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
show_more: "Display more"
|
||||
show_less: "Display less"
|
||||
documentation: "Documentation"
|
||||
store_product_item:
|
||||
unit: "unit"
|
||||
cart:
|
||||
my_cart: "My Cart"
|
||||
cart_button:
|
||||
|
Loading…
x
Reference in New Issue
Block a user