1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

Clients' store categories

This commit is contained in:
vincent 2022-08-25 17:08:43 +02:00
parent e0dc008d4c
commit d602087710
8 changed files with 236 additions and 140 deletions

View File

@ -108,7 +108,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
};
/**
* Filter: toggle hidden products visibility
* Filter: toggle non-available products visibility
*/
const toggleVisible = (checked: boolean) => {
setFilterVisible(checked);
@ -275,7 +275,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
label={t('app.admin.store.products.filter_categories')}
>
<div className='content'>
<div className="list scrollbar">
<div className="list u-scrollbar">
{productCategories.map(pc => (
<label key={pc.id} className={pc.parent_id ? 'offset' : ''}>
<input type="checkbox" checked={filters.categories.includes(pc)} onChange={(event) => handleSelectCategory(pc, event.target.checked)} />
@ -293,7 +293,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
label={t('app.admin.store.products.filter_machines')}
>
<div className='content'>
<div className="list scrollbar">
<div className="list u-scrollbar">
{machines.map(m => (
<label key={m.value}>
<input type="checkbox" checked={filters.machines.includes(m)} onChange={(event) => handleSelectMachine(m, event.target.checked)} />
@ -395,16 +395,3 @@ interface Filters {
internalStock: Stock,
externalStock: Stock
}
// Styles the React-select component
const customStyles = {
control: base => ({
...base,
width: '20ch',
border: 'none',
backgroundColor: 'transparent'
}),
indicatorSeparator: () => ({
display: 'none'
})
};

View File

@ -26,7 +26,6 @@ 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);
@ -170,7 +169,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
<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')}
{t('app.public.store_product_item.add_to_cart')}
</FabButton>
</div>
}

View File

@ -38,6 +38,8 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
const [products, setProducts] = useState<Array<Product>>([]);
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
const [categoriesTree, setCategoriesTree] = useState<ParentCategory[]>([]);
const [activeCategory, setActiveCategory] = useState<ActiveCategory>();
const [machines, setMachines] = useState<checklistOption[]>([]);
const [accordion, setAccordion] = useState({});
@ -49,18 +51,8 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
});
ProductCategoryAPI.index().then(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);
setProductCategories(data);
formatCategories(data);
}).catch(() => {
onError(t('app.public.store.unexpected_error_occurred'));
});
@ -82,6 +74,29 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
}
}, [currentUser]);
/**
* Create categories tree (parent/children)
*/
const formatCategories = (list: ProductCategory[]) => {
const tree = [];
const parents = list.filter(c => !c.parent_id);
const getChildren = (id) => {
return list.filter(c => c.parent_id === id);
};
parents.forEach(p => {
tree.push({ parent: p, children: getChildren(p.id) });
});
setCategoriesTree(tree);
};
/**
* Filter by category
*/
const filterCategory = (id: number, parent?: number) => {
setActiveCategory({ id, parent });
console.log('Filter by category:', productCategories.find(c => c.id === id).name);
};
/**
* Apply filters
*/
@ -121,7 +136,7 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
};
/**
* Filter: toggle hidden products visibility
* Filter: toggle non-available products visibility
*/
const toggleVisible = (checked: boolean) => {
console.log('Display in stock only:', checked);
@ -129,50 +144,79 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
return (
<div className="store">
<div className='store-filters'>
<header>
<h3>{t('app.public.store.products.filter')}</h3>
<div className='grpBtn'>
<FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
<ul className="breadcrumbs">
<li>
<span onClick={() => setActiveCategory(null)}>{t('app.public.store.products.all_products')}</span>
</li>
{activeCategory?.parent &&
<li>
<span onClick={() => filterCategory(activeCategory?.parent)}>
{productCategories.find(c => c.id === activeCategory.parent).name}
</span>
</li>
}
{activeCategory?.id &&
<li>
<span onClick={() => filterCategory(activeCategory?.id, activeCategory?.parent)}>
{productCategories.find(c => c.id === activeCategory.id).name}
</span>
</li>
}
</ul>
<aside className='store-filters'>
<div className="categories">
<header>
<h3>{t('app.public.store.products.filter_categories')}</h3>
</header>
<div className="list u-scrollbar">
{categoriesTree.map(c =>
<div key={c.parent.id} className={`parent ${activeCategory?.id === c.parent.id || activeCategory?.parent === c.parent.id ? 'is-active' : ''}`}>
<p onClick={() => filterCategory(c.parent.id)}>
{c.parent.name}<span>(count)</span>
</p>
{c.children.length > 0 &&
<div className='children'>
{c.children.map(ch =>
<p key={ch.id}
className={activeCategory?.id === ch.id ? 'is-active' : ''}
onClick={() => filterCategory(ch.id, c.parent.id)}>
{ch.name}<span>(count)</span>
</p>
)}
</div>
}
</div>
)}
</div>
</header>
<div className="accordion">
<AccordionItem id={0}
isOpen={accordion[0]}
onChange={handleAccordion}
label={t('app.public.store.products.filter_categories')}
>
<div className='content'>
<div className="list scrollbar">
{productCategories.map(pc => (
<label key={pc.id} className={pc.parent_id ? 'offset' : ''}>
<input type="checkbox" />
<p>{pc.name}</p>
</label>
))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.public.store.products.filter_apply')}</FabButton>
</div>
</AccordionItem>
<AccordionItem id={1}
isOpen={accordion[1]}
onChange={handleAccordion}
label={t('app.public.store.products.filter_machines')}
>
<div className='content'>
<div className="list scrollbar">
{machines.map(m => (
<label key={m.value}>
<input type="checkbox" />
<p>{m.label}</p>
</label>
))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.public.store.products.filter_apply')}</FabButton>
</div>
</AccordionItem>
</div>
</div>
<div className='filters'>
<header>
<h3>{t('app.public.store.products.filter')}</h3>
<div className='grpBtn'>
<FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
</div>
</header>
<div className="accordion">
<AccordionItem id={1}
isOpen={accordion[1]}
onChange={handleAccordion}
label={t('app.public.store.products.filter_machines')}
>
<div className='content'>
<div className="list u-scrollbar">
{machines.map(m => (
<label key={m.value}>
<input type="checkbox" />
<p>{m.label}</p>
</label>
))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.public.store.products.filter_apply')}</FabButton>
</div>
</AccordionItem>
</div>
</div>
</aside>
<div className='store-products-list'>
<ProductsListHeader
productsCount={products.length}
@ -181,17 +225,6 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
switchLabel={t('app.public.store.products.in_stock_only')}
onSwitch={toggleVisible}
/>
<div className='features'>
<div className='features-item'>
<p>feature name</p>
<button><i className="fa fa-times" /></button>
</div>
<div className='features-item'>
<p>long feature name</p>
<button><i className="fa fa-times" /></button>
</div>
</div>
<div className="products-grid">
{products.map((product) => (
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} />
@ -224,3 +257,12 @@ const StoreWrapper: React.FC<StoreProps> = (props) => {
};
Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'currentUser']));
interface ActiveCategory {
id: number,
parent: number
}
interface ParentCategory {
parent: ProductCategory,
children: ProductCategory[]
}

View File

@ -66,4 +66,26 @@
@include text-lg(600);
color: var(--gray-hard-darkest) !important;
}
}
// Custom scrollbar
.u-scrollbar {
&::-webkit-scrollbar-track
{
border-radius: 6px;
background-color: #d9d9d9;
}
&::-webkit-scrollbar
{
width: 12px;
background-color: #ffffff;
}
&::-webkit-scrollbar-thumb
{
border-radius: 6px;
background-color: #2d2d2d;
border: 2px solid #d9d9d9
}
}

View File

@ -1,7 +1,56 @@
.store-filters {
grid-column: 1 / 4;
padding-top: 1.6rem;
border-top: 1px solid var(--gray-soft-dark);
.categories {
margin-bottom: 3.2rem;
p {
display: flex;
align-items: baseline;
cursor: pointer;
span {
margin-left: 0.8rem;
@include text-xs;
color: var(--information);
}
}
.parent {
& > p {
margin-bottom: 2.4rem;
@include text-base(500);
color: var(--gray-hard);
}
&.is-active > p {
@include text-base(600);
color: var(--information);
.children {
max-height: 1000px;
}
}
&.is-active .children {
max-height: 1000px;
margin: -0.8rem 0 1.6rem;
transition: max-height 500ms ease-in-out;
}
}
.children {
max-height: 0;
overflow: hidden;
p {
margin-bottom: 1.6rem;
@include text-base(400);
color: var(--gray-hard-light);
&.is-active {
background-color: var(--gray-soft-light);
}
}
}
}
.filters {
padding-top: 1.6rem;
border-top: 1px solid var(--gray-soft-dark);
}
header {
@include header();
@ -20,20 +69,7 @@
.content { max-height: 0; }
header svg { transform: rotateZ(180deg); }
}
& > input[type=checkbox] {
position: absolute;
width: 100%;
z-index: 1;
opacity: 0;
cursor: pointer;
}
& > input[type=checkbox]:checked ~ .content {
max-height: 0;
}
& > input[type=checkbox]:checked ~ header svg {
transform: rotateZ(180deg);
}
header {
width: 100%;
padding: 0;
@ -81,26 +117,4 @@
}
}
}
}
// Custom scrollbar
.scrollbar {
&::-webkit-scrollbar-track
{
border-radius: 6px;
background-color: #d9d9d9;
}
&::-webkit-scrollbar
{
width: 12px;
background-color: #ffffff;
}
&::-webkit-scrollbar-thumb
{
border-radius: 6px;
background-color: #2d2d2d;
border: 2px solid #d9d9d9
}
}

View File

@ -1,7 +1,10 @@
.store-products-list {
grid-column: 4 / -1;
display: grid;
grid-template-columns: 1fr;
gap: 2.4rem 0;
.features {
margin: 2.4rem 0 1.6rem;
display: flex;
flex-wrap: wrap;
align-items: center;
@ -26,5 +29,4 @@
}
}
}
}

View File

@ -2,11 +2,38 @@
.store-product {
max-width: 1600px;
@include grid-col(12);
gap: 3.2rem;
gap: 2.4rem 3.2rem;
align-items: flex-start;
margin: 0 auto;
padding-bottom: 6rem;
}
.store {
.breadcrumbs {
grid-column: 1 / -1;
padding: 0.8rem 1.6rem;
display: flex;
list-style: none;
border-radius: var(--border-radius-sm);
background-color: var(--gray-soft-light);
li:not(:last-of-type)::after {
margin: 0 2.4rem;
content: "\f054";
font-family: 'Font Awesome 5 Free';
font-size: 1.2rem;
font-weight: 900;
color: var(--gray-hard-darkest);
}
li:last-of-type:not(:first-of-type) span {
color: var(--information);
}
span {
color: var(--gray-hard-light);
cursor: pointer;
}
}
}
.store-product {
--status-color: var(--success);
&.low { --status-color: var(--alert-light); }

View File

@ -379,27 +379,30 @@ en:
fablab_store: "FabLab Store"
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
products:
filter: "Filter"
filter_clear: "Clear all"
filter_apply: "Apply"
filter_categories: "By categories"
filter_machines: "By machines"
filter_keywords_reference: "By keywords or reference"
filter_stock: "By stock status"
filter_stock_from: "From"
filter_stock_to: "to"
in_stock_only: "Available products only"
sort:
name_az: "A-Z"
name_za: "Z-A"
price_low: "Price: low to high"
price_high: "Price: high to low"
all_products: "All the products"
filter: "Filter"
filter_clear: "Clear all"
filter_apply: "Apply"
filter_categories: "Categories"
filter_machines: "By machines"
filter_keywords_reference: "By keywords or reference"
in_stock_only: "Available products only"
sort:
name_az: "A-Z"
name_za: "Z-A"
price_low: "Price: low to high"
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:
available: "Available"
limited_stock: "Limited stock"
out_of_stock: "Out of stock"
add: "Add"
add_to_cart: "Add to cart"
unit: "unit"
cart:
my_cart: "My Cart"