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:
parent
e0dc008d4c
commit
d602087710
@ -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'
|
||||
})
|
||||
};
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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,7 +144,52 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
|
||||
|
||||
return (
|
||||
<div className="store">
|
||||
<div className='store-filters'>
|
||||
<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>
|
||||
</div>
|
||||
<div className='filters'>
|
||||
<header>
|
||||
<h3>{t('app.public.store.products.filter')}</h3>
|
||||
<div className='grpBtn'>
|
||||
@ -137,30 +197,13 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
|
||||
</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">
|
||||
<div className="list u-scrollbar">
|
||||
{machines.map(m => (
|
||||
<label key={m.value}>
|
||||
<input type="checkbox" />
|
||||
@ -173,6 +216,7 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
|
||||
</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[]
|
||||
}
|
||||
|
@ -67,3 +67,25 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,7 +1,56 @@
|
||||
.store-filters {
|
||||
grid-column: 1 / 4;
|
||||
|
||||
.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();
|
||||
@ -21,19 +70,6 @@
|
||||
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;
|
||||
@ -82,25 +118,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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); }
|
||||
|
@ -379,15 +379,13 @@ en:
|
||||
fablab_store: "FabLab Store"
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
products:
|
||||
all_products: "All the products"
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
filter_categories: "By categories"
|
||||
filter_categories: "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"
|
||||
@ -400,6 +398,11 @@ en:
|
||||
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user