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

Cleanup files

This commit is contained in:
vincent 2022-08-23 18:55:49 +02:00
parent 857261ba62
commit 29993b0ec9
23 changed files with 713 additions and 711 deletions

View File

@ -224,21 +224,21 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
</div> </div>
</header> </header>
<form className="product-form" onSubmit={onSubmit}> <form className="product-form" onSubmit={onSubmit}>
<div className="layout"> <div className="subgrid">
<FormInput id="name" <FormInput id="name"
register={register} register={register}
rules={{ required: true }} rules={{ required: true }}
formState={formState} formState={formState}
onChange={handleNameChange} onChange={handleNameChange}
label={t('app.admin.store.product_form.name')} label={t('app.admin.store.product_form.name')}
className='span-7' /> className="span-7" />
<FormInput id="sku" <FormInput id="sku"
register={register} register={register}
formState={formState} formState={formState}
label={t('app.admin.store.product_form.sku')} label={t('app.admin.store.product_form.sku')}
className='span-3' /> className="span-3" />
</div> </div>
<div className="layout"> <div className="subgrid">
<FormInput id="slug" <FormInput id="slug"
register={register} register={register}
rules={{ required: true }} rules={{ required: true }}
@ -256,7 +256,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<hr /> <hr />
<div className="price-data"> <div className="price-data">
<div className="layout"> <div className="price-data-header">
<h4 className='span-7'>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4> <h4 className='span-7'>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
<FormSwitch control={control} <FormSwitch control={control}
id="is_active_price" id="is_active_price"
@ -265,22 +265,20 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
onChange={toggleIsActivePrice} onChange={toggleIsActivePrice}
className='span-3' /> className='span-3' />
</div> </div>
{isActivePrice && <div className="price-fields"> {isActivePrice && <div className="price-data-content">
<div className="flex"> <FormInput id="amount"
<FormInput id="amount" type="number"
type="number" register={register}
register={register} rules={{ required: true, min: 0.01 }}
rules={{ required: true, min: 0.01 }} step={0.01}
step={0.01} formState={formState}
formState={formState} label={t('app.admin.store.product_form.price')} />
label={t('app.admin.store.product_form.price')} /> <FormInput id="quantity_min"
<FormInput id="quantity_min" type="number"
type="number" rules={{ required: true }}
rules={{ required: true }} register={register}
register={register} formState={formState}
formState={formState} label={t('app.admin.store.product_form.quantity_min')} />
label={t('app.admin.store.product_form.quantity_min')} />
</div>
</div>} </div>}
</div> </div>

View File

@ -0,0 +1,100 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FormatLib from '../../lib/format';
import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product';
import { PencilSimple, Trash } from 'phosphor-react';
import noImage from '../../../../images/no_image.png';
interface ProductItemProps {
product: Product,
onEdit: (product: Product) => void,
onDelete: (productId: number) => void,
}
/**
* This component shows a product item in the admin view
*/
export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDelete }) => {
const { t } = useTranslation('admin');
/**
* Get the main image
*/
const thumbnail = () => {
const image = product.product_images_attributes
.find(att => att.is_main);
return image;
};
/**
* Init the process of editing the given product
*/
const editProduct = (product: Product): () => void => {
return (): void => {
onEdit(product);
};
};
/**
* Init the process of delete the given product
*/
const deleteProduct = (productId: number): () => void => {
return (): void => {
onDelete(productId);
};
};
/**
* 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 (
<div className={`product-item ${statusColor(product)}`}>
<div className='itemInfo'>
{/* TODO: image size version ? */}
<img src={thumbnail()?.attachment_url || noImage} alt='' className='itemInfo-thumbnail' />
<p className="itemInfo-name">{product.name}</p>
</div>
<div className='details'>
<span className={`visibility ${product.is_active ? 'is-active' : ''}`}>
{product.is_active
? t('app.admin.store.product_item.visible')
: t('app.admin.store.product_item.hidden')
}
</span>
<div className='stock'>
<span>{t('app.admin.store.product_item.stock.internal')}</span>
<p>{product.stock.internal}</p>
</div>
<div className='stock'>
<span>{t('app.admin.store.product_item.stock.external')}</span>
<p>{product.stock.external}</p>
</div>
{product.amount &&
<div className='price'>
<p>{FormatLib.price(product.amount)}</p>
<span>/ {t('app.admin.store.product_item.unit')}</span>
</div>
}
</div>
<div className='actions'>
<div className='manage'>
<FabButton className='edit-btn' onClick={editProduct(product)}>
<PencilSimple size={20} weight="fill" />
</FabButton>
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}>
<Trash size={20} weight="fill" />
</FabButton>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Select from 'react-select';
import Switch from 'react-switch';
interface ProductsListHeaderProps {
productsCount: number,
selectOptions: selectOption[],
onSelectOptionsChange: (option: selectOption) => void,
switchLabel?: string,
onSwitch: (boolean) => void
}
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: number, label: string };
/**
* Renders an accordion item
*/
export const ProductsListHeader: React.FC<ProductsListHeaderProps> = ({ productsCount, selectOptions, onSelectOptionsChange, switchLabel, onSwitch }) => {
const { t } = useTranslation('admin');
// Styles the React-select component
const customStyles = {
control: base => ({
...base,
width: '20ch',
border: 'none',
backgroundColor: 'transparent'
}),
indicatorSeparator: () => ({
display: 'none'
})
};
return (
<div className='products-list-header'>
<div className='count'>
<p>{t('app.admin.store.products_list_header.result_count')}<span>{productsCount}</span></p>
</div>
<div className="display">
<div className='sort'>
<p>{t('app.admin.store.products_list_header.display_options')}</p>
<Select
options={selectOptions}
onChange={evt => onSelectOptionsChange(evt)}
styles={customStyles}
/>
</div>
<div className='visibility'>
<label>
<span>{switchLabel || t('app.admin.store.products_list_header.visible_only')}</span>
<Switch
checked={true}
onChange={(checked) => onSwitch(checked)}
width={40}
height={19}
uncheckedIcon={false}
checkedIcon={false}
handleDiameter={15} />
</label>
</div>
</div>
</div>
);
};

View File

@ -1,106 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FormatLib from '../../lib/format';
import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product';
import { PencilSimple, Trash } from 'phosphor-react';
import noImage from '../../../../images/no_image.png';
interface ProductsListProps {
products: Array<Product>,
onEdit: (product: Product) => void,
onDelete: (productId: number) => void,
}
/**
* This component shows a list of all Products
*/
export const ProductsList: React.FC<ProductsListProps> = ({ products, onEdit, onDelete }) => {
const { t } = useTranslation('admin');
/**
* TODO, document this method
*/
const thumbnail = (id: number) => {
const image = products
?.find(p => p.id === id)
.product_images_attributes
.find(att => att.is_main);
return image;
};
/**
* Init the process of editing the given product
*/
const editProduct = (product: Product): () => void => {
return (): void => {
onEdit(product);
};
};
/**
* Init the process of delete the given product
*/
const deleteProduct = (productId: number): () => void => {
return (): void => {
onDelete(productId);
};
};
/**
* 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 (
<>
{products.map((product) => (
<div className={`products-list-item ${statusColor(product)}`} key={product.id}>
<div className='itemInfo'>
{/* TODO: image size version ? */}
<img src={thumbnail(product.id)?.attachment_url || noImage} alt='' className='itemInfo-thumbnail' />
<p className="itemInfo-name">{product.name}</p>
</div>
<div className='details'>
<span className={`visibility ${product.is_active ? 'is-active' : ''}`}>
{product.is_active
? t('app.admin.store.products_list.visible')
: t('app.admin.store.products_list.hidden')
}
</span>
<div className='stock'>
<span>{t('app.admin.store.products_list.stock.internal')}</span>
<p>{product.stock.internal}</p>
</div>
<div className='stock'>
<span>{t('app.admin.store.products_list.stock.external')}</span>
<p>{product.stock.external}</p>
</div>
{product.amount &&
<div className='price'>
<p>{FormatLib.price(product.amount)}</p>
<span>/ {t('app.admin.store.products_list.unit')}</span>
</div>
}
</div>
<div className='actions'>
<div className='manage'>
<FabButton className='edit-btn' onClick={editProduct(product)}>
<PencilSimple size={20} weight="fill" />
</FabButton>
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}>
<Trash size={20} weight="fill" />
</FabButton>
</div>
</div>
</div>
))}
</>
);
};

View File

@ -9,14 +9,13 @@ import { IApplication } from '../../models/application';
import { Product } from '../../models/product'; import { Product } from '../../models/product';
import { ProductCategory } from '../../models/product-category'; import { ProductCategory } from '../../models/product-category';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { ProductsList } from './products-list'; import { ProductItem } from './product-item';
import ProductAPI from '../../api/product'; import ProductAPI from '../../api/product';
import ProductCategoryAPI from '../../api/product-category'; import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine'; import MachineAPI from '../../api/machine';
import { AccordionItem } from './accordion-item'; import { AccordionItem } from './accordion-item';
import { X } from 'phosphor-react'; import { X } from 'phosphor-react';
import Switch from 'react-switch'; import { ProductsListHeader } from './products-list-header';
import Select from 'react-select';
declare const Application: IApplication; declare const Application: IApplication;
@ -31,7 +30,7 @@ interface ProductsProps {
type selectOption = { value: number, label: string }; type selectOption = { value: number, label: string };
/** /**
* This component shows all Products and filter * This component shows the admin view of the store
*/ */
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => { const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
@ -173,9 +172,8 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
/** /**
* Display option: sorting * Display option: sorting
*/ */
const handleSorting = (value: number) => { const handleSorting = (option: selectOption) => {
setSortOption(value); console.log('Sort option:', option);
setUpdate(true);
}; };
/** /**
@ -226,9 +224,9 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
const buildOptions = (): Array<selectOption> => { const buildOptions = (): Array<selectOption> => {
return [ return [
{ value: 0, label: t('app.admin.store.products.sort.name_az') }, { value: 0, label: t('app.admin.store.products.sort.name_az') },
{ value: 1, label: t('app.admin.store.products.sort.name_za') } { value: 1, label: t('app.admin.store.products.sort.name_za') },
// { value: 2, label: t('app.admin.store.products.sort.price_low') }, { value: 2, label: t('app.admin.store.products.sort.price_low') },
// { value: 3, label: t('app.admin.store.products.sort.price_high') } { value: 3, label: t('app.admin.store.products.sort.price_high') }
]; ];
}; };
@ -263,101 +261,82 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
<FabButton className="main-action-btn" onClick={newProduct}>{t('app.admin.store.products.create_a_product')}</FabButton> <FabButton className="main-action-btn" onClick={newProduct}>{t('app.admin.store.products.create_a_product')}</FabButton>
</div> </div>
</header> </header>
<div className='layout'> <div className='store-filters'>
<div className='products-filters span-3'> <header>
<header> <h3>{t('app.admin.store.products.filter')}</h3>
<h3>{t('app.admin.store.products.filter')}</h3> <div className='grpBtn'>
<div className='grpBtn'> <FabButton onClick={clearAllFilters} className="is-black">{t('app.admin.store.products.filter_clear')}</FabButton>
<FabButton onClick={clearAllFilters} className="is-black">{t('app.admin.store.products.filter_clear')}</FabButton> </div>
</div> </header>
</header> <div className='accordion'>
<div className='accordion'> <AccordionItem id={0}
<AccordionItem id={0} isOpen={accordion[0]}
isOpen={accordion[0]} onChange={handleAccordion}
onChange={handleAccordion} label={t('app.admin.store.products.filter_categories')}
label={t('app.admin.store.products.filter_categories')} >
> <div className='content'>
<div className='content'> <div className="list scrollbar">
<div className="list scrollbar"> {productCategories.map(pc => (
{productCategories.map(pc => ( <label key={pc.id} className={pc.parent_id ? 'offset' : ''}>
<label key={pc.id} className={pc.parent_id ? 'offset' : ''}> <input type="checkbox" checked={filters.categories.includes(pc)} onChange={(event) => handleSelectCategory(pc, event.target.checked)} />
<input type="checkbox" checked={filters.categories.includes(pc)} onChange={(event) => handleSelectCategory(pc, event.target.checked)} /> <p>{pc.name}</p>
<p>{pc.name}</p> </label>
</label> ))}
))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div> </div>
</AccordionItem> <FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div>
</AccordionItem>
<AccordionItem id={1} <AccordionItem id={1}
isOpen={accordion[1]} isOpen={accordion[1]}
onChange={handleAccordion} onChange={handleAccordion}
label={t('app.admin.store.products.filter_machines')} label={t('app.admin.store.products.filter_machines')}
> >
<div className='content'> <div className='content'>
<div className="list scrollbar"> <div className="list scrollbar">
{machines.map(m => ( {machines.map(m => (
<label key={m.value}> <label key={m.value}>
<input type="checkbox" checked={filters.machines.includes(m)} onChange={(event) => handleSelectMachine(m, event.target.checked)} /> <input type="checkbox" checked={filters.machines.includes(m)} onChange={(event) => handleSelectMachine(m, event.target.checked)} />
<p>{m.label}</p> <p>{m.label}</p>
</label> </label>
))} ))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div> </div>
</AccordionItem> <FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div> </div>
</AccordionItem>
</div> </div>
<div className='products-list span-7'> </div>
<div className='status'> <div className='store-products-list'>
<div className='count'> <ProductsListHeader
<p>{t('app.admin.store.products.result_count')}<span>{filteredProductsList.length}</span></p> productsCount={filteredProductsList.length}
selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting}
onSwitch={toggleVisible}
/>
<div className='features'>
{features.categories.map(c => (
<div key={c.id} className='features-item'>
<p>{c.name}</p>
<button onClick={() => handleSelectCategory(c, false, true)}><X size={16} weight="light" /></button>
</div> </div>
<div className="display"> ))}
<div className='sort'> {features.machines.map(m => (
<p>{t('app.admin.store.products.display_options')}</p> <div key={m.value} className='features-item'>
<Select <p>{m.label}</p>
options={buildOptions()} <button onClick={() => handleSelectMachine(m, false, true)}><X size={16} weight="light" /></button>
onChange={evt => handleSorting(evt.value)}
value={buildOptions[sortOption]}
styles={customStyles}
/>
</div>
<div className='visibility'>
<label>
<span>{t('app.admin.store.products.visible_only')}</span>
<Switch
checked={filterVisible}
onChange={(checked) => toggleVisible(checked)}
width={40}
height={19}
uncheckedIcon={false}
checkedIcon={false}
handleDiameter={15} />
</label>
</div>
</div> </div>
</div> ))}
<div className='features'> </div>
{features.categories.map(c => (
<div key={c.id} className='features-item'> <div className="products-list">
<p>{c.name}</p> {filteredProductsList.map((product) => (
<button onClick={() => handleSelectCategory(c, false, true)}><X size={16} weight="light" /></button> <ProductItem
</div> key={product.id}
))} product={product}
{features.machines.map(m => ( onEdit={editProduct}
<div key={m.value} className='features-item'> onDelete={deleteProduct}
<p>{m.label}</p> />
<button onClick={() => handleSelectMachine(m, false, true)}><X size={16} weight="light" /></button> ))}
</div>
))}
</div>
<ProductsList
products={filteredProductsList}
onEdit={editProduct}
onDelete={deleteProduct}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,11 +5,16 @@ import { Loader } from '../base/loader';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product'; import { Product } from '../../models/product';
import { ProductCategory } from '../../models/product-category';
import ProductAPI from '../../api/product'; import ProductAPI from '../../api/product';
import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine';
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'; import { emitCustomEvent } from 'react-custom-events';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { AccordionItem } from './accordion-item';
import { ProductsListHeader } from './products-list-header';
declare const Application: IApplication; declare const Application: IApplication;
@ -17,6 +22,11 @@ interface StoreProps {
onError: (message: string) => void, onError: (message: string) => void,
currentUser: User, currentUser: User,
} }
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: number, label: string };
/** /**
* This component shows public store * This component shows public store
@ -27,6 +37,9 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
const { cart, setCart, reloadCart } = useCart(); const { cart, setCart, reloadCart } = useCart();
const [products, setProducts] = useState<Array<Product>>([]); const [products, setProducts] = useState<Array<Product>>([]);
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
const [machines, setMachines] = useState<checklistOption[]>([]);
const [accordion, setAccordion] = useState({});
useEffect(() => { useEffect(() => {
ProductAPI.index({ is_active: true }).then(data => { ProductAPI.index({ is_active: true }).then(data => {
@ -34,6 +47,18 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
}).catch(() => { }).catch(() => {
onError(t('app.public.store.unexpected_error_occurred')); onError(t('app.public.store.unexpected_error_occurred'));
}); });
ProductCategoryAPI.index().then(data => {
setProductCategories(data);
}).catch(() => {
onError(t('app.public.store.unexpected_error_occurred'));
});
MachineAPI.index({ disabled: false }).then(data => {
setMachines(buildChecklistOptions(data));
}).catch(() => {
onError(t('app.public.store.unexpected_error_occurred'));
});
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -46,53 +71,139 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
} }
}, [currentUser]); }, [currentUser]);
/**
* Apply filters
*/
const applyFilters = () => {
console.log('Filter products');
};
/**
* Clear filters
*/
const clearAllFilters = () => {
console.log('Clear filters');
};
/**
* Open/close accordion items
*/
const handleAccordion = (id, state) => {
setAccordion({ ...accordion, [id]: state });
};
/**
* Creates sorting options to the react-select format
*/
const buildOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.public.store.products.sort.name_az') },
{ value: 1, label: t('app.public.store.products.sort.name_za') },
{ value: 2, label: t('app.public.store.products.sort.price_low') },
{ value: 3, label: t('app.public.store.products.sort.price_high') }
];
};
/**
* Display option: sorting
*/
const handleSorting = (option: selectOption) => {
console.log('Sort option:', option);
};
/**
* Filter: toggle hidden products visibility
*/
const toggleVisible = (checked: boolean) => {
console.log('Display in stock only:', checked);
};
return ( return (
<div className="store"> <div className="store">
<div className='layout'> <div className='store-filters'>
<div className='store-filters span-3'> <header>
<header> <h3>{t('app.public.store.products.filter')}</h3>
<h3>Filtrer</h3> <div className='grpBtn'>
<div className='grpBtn'> <FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
<FabButton className="is-black">Clear</FabButton> </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> </div>
</header> </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='store-products-list'>
<ProductsListHeader
productsCount={products.length}
selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting}
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>
<div className='store-products-list span-7'>
<div className='status'>
<div className='count'>
<p>Result count: <span>{products.length}</span></p>
</div>
<div className="">
<div className='sort'>
<p>Display options:</p>
</div>
<div className='visibility'>
</div> <div className="products-grid">
</div> {products.map((product) => (
</div> <StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} />
<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">
{products.map((product) => (
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} />
))}
</div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
/**
* Option format, expected by checklist
*/
type checklistOption = { value: number, label: string };
/**
* Convert the provided array of items to the checklist format
*/
const buildChecklistOptions = (items: Array<{ id?: number, name: string }>): Array<checklistOption> => {
return items.map(t => {
return { value: t.id, label: t.name };
});
};
const StoreWrapper: React.FC<StoreProps> = (props) => { const StoreWrapper: React.FC<StoreProps> = (props) => {
return ( return (
<Loader> <Loader>

View File

@ -89,12 +89,14 @@
@import "modules/settings/user-validation-setting"; @import "modules/settings/user-validation-setting";
@import "modules/socials/fab-socials"; @import "modules/socials/fab-socials";
@import "modules/store/_utilities"; @import "modules/store/_utilities";
@import "modules/store/manage-product-category";
@import "modules/store/product-categories"; @import "modules/store/product-categories";
@import "modules/store/product-form"; @import "modules/store/product-form";
@import "modules/store/products-filters"; @import "modules/store/products-grid";
@import "modules/store/products-list-header";
@import "modules/store/products-list"; @import "modules/store/products-list";
@import "modules/store/products"; @import "modules/store/products";
@import "modules/store/store-filters";
@import "modules/store/store-products-list";
@import "modules/store/store"; @import "modules/store/store";
@import "modules/subscriptions/free-extend-modal"; @import "modules/subscriptions/free-extend-modal";
@import "modules/subscriptions/renew-modal"; @import "modules/subscriptions/renew-modal";

View File

@ -11,4 +11,55 @@
color: currentColor; color: currentColor;
box-shadow: none; box-shadow: none;
} }
}
@mixin grid-col($col-count) {
width: 100%;
display: grid;
grid-template-columns: repeat($col-count, minmax(0, 1fr));
}
.back-btn {
margin: 2.4rem 0;
padding: 0.4rem 0.8rem;
display: inline-flex;
align-items: center;
background-color: var(--gray-soft-darkest);
border-radius: var(--border-radius-sm);
color: var(--gray-soft-lightest);
i { margin-right: 0.8rem; }
&:hover {
color: var(--gray-soft-lightest);
background-color: var(--gray-hard-lightest);
cursor: pointer;
}
}
.main-action-btn {
background-color: var(--main);
color: var(--gray-soft-lightest);
border: none;
&:hover { opacity: 0.75; }
}
@mixin header {
padding: 2.4rem 0;
display: flex;
justify-content: space-between;
align-items: center;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
h2 {
margin: 0;
@include title-lg;
color: var(--gray-hard-darkest) !important;
}
h3 {
margin: 0;
@include text-lg(600);
color: var(--gray-hard-darkest) !important;
}
} }

View File

@ -1,35 +0,0 @@
<!-- Drop options -->
## [A] Single |> [B] Single
[A] = index de [B]
offset && [A] child de [B]
<!--## [A] Single || Child |> [B] Parent
[A] = index de [B]
[A] child de [B]-->
<!--## [A] Single || Child |> [B] Child
[A] = index de [B]
[A] même parent que [B]-->
## [A] Child |> [B] Single
[A] = index de [B]
offset
? [A] child de [B]
: [A] Single
<!--## [A] Parent |> [B] Single
[A] = index de [B]-->
<!--## [A] Parent |> [B] Parent
down
? [A] = index du dernier child de [B]
: [A] = index de [B]-->
<!--## [A] Parent |> [B] Child
down
? [A] = index du dernier child de [B]
: [A] = index du parent de [B]-->
## [A] Single |> [A]
offset && [A] child du précédant parent

View File

@ -1,3 +0,0 @@
.manage-product-category {
}

View File

@ -1,40 +1,30 @@
.product-categories { .product-categories {
max-width: 1300px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding-bottom: 6rem;
@include grid-col(12);
gap: 0 3.2rem;
header { header {
padding: 2.4rem 0; @include header();
display: flex; grid-column: 2 / -2;
justify-content: space-between;
align-items: center;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
.create-button {
background-color: var(--gray-hard-darkest);
border-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest);
&:hover {
background-color: var(--gray-hard-light);
border-color: var(--gray-hard-light);
}
}
}
h2 {
margin: 0;
@include title-lg;
color: var(--gray-hard-darkest) !important;
}
}
.main-action-btn { .create-button {
background-color: var(--main); background-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest); border-color: var(--gray-hard-darkest);
border: none; color: var(--gray-soft-lightest);
&:hover { opacity: 0.75; } &:hover {
background-color: var(--gray-hard-light);
border-color: var(--gray-hard-light);
}
}
}
.fab-alert {
grid-column: 2 / -2;
} }
&-tree { &-tree {
grid-column: 2 / -2;
& > *:not(:first-child) { & > *:not(:first-child) {
margin-top: 1.6rem; margin-top: 1.6rem;
} }

View File

@ -1,4 +1,5 @@
.product-form { .product-form {
grid-column: 2 / -2;
h4 { h4 {
margin: 0 0 2.4rem; margin: 0 0 2.4rem;
@include title-base; @include title-base;
@ -7,6 +8,18 @@
margin: 4.8rem 0; margin: 4.8rem 0;
} }
.subgrid {
@include grid-col(10);
gap: 3.2rem;
align-items: flex-end;
}
.span-3 { grid-column: span 3; }
.span-7 { grid-column: span 7; }
& > div {
grid-column: 2 / -2;
}
.flex { .flex {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -16,23 +29,17 @@
flex: 1 1 320px; flex: 1 1 320px;
} }
} }
.layout {
@media (max-width: 1023px) {
.span-3,
.span-7 {
flex-basis: 50%;
}
}
@media (max-width: 767px) {
flex-wrap: wrap;
}
}
.price-data { .price-data-header {
.layout { @include grid-col(10);
align-items: center; gap: 3.2rem;
} align-items: center;
}
.price-data-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 0 3.2rem;
align-items: flex-end;
} }
.product-images, .product-images,

View File

@ -0,0 +1,9 @@
.products-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 3.2rem;
.store-product-item {
color: tomato;
}
}

View File

@ -0,0 +1,51 @@
.products-list-header {
padding: 0.8rem 2.4rem;
display: flex;
justify-content: space-between;
background-color: var(--gray-soft);
border-radius: var(--border-radius);
p { margin: 0; }
.count {
display: flex;
align-items: center;
p {
@include text-sm;
span {
margin-left: 1.6rem;
@include text-lg(600);
}
}
}
.display {
display: flex;
align-items: center;
& > *:not(:first-child) {
margin-left: 2rem;
padding-left: 2rem;
border-left: 1px solid var(--gray-hard-darkest);
}
.sort {
display: flex;
align-items: center;
p { margin-right: 0.8rem; }
}
.visibility {
display: flex;
align-items: center;
label {
margin: 0;
display: flex;
align-items: center;
font-weight: 400;
cursor: pointer;
span {
margin-right: 1rem;
}
}
}
}
}

View File

@ -1,88 +1,13 @@
.products-list { .products-list {
.status { & > *:not(:first-child) {
padding: 0.8rem 2.4rem; margin-top: 1.6rem;
display: flex;
justify-content: space-between;
background-color: var(--gray-soft);
border-radius: var(--border-radius);
p { margin: 0; }
.count {
display: flex;
align-items: center;
p {
@include text-sm;
span {
margin-left: 1.6rem;
@include text-lg(600);
}
}
}
.display {
display: flex;
align-items: center;
& > *:not(:first-child) {
margin-left: 2rem;
padding-left: 2rem;
border-left: 1px solid var(--gray-hard-darkest);
}
.sort {
display: flex;
align-items: center;
p { margin-right: 0.8rem; }
}
.visibility {
display: flex;
align-items: center;
label {
margin: 0;
display: flex;
align-items: center;
font-weight: 400;
cursor: pointer;
span {
margin-right: 1rem;
}
}
}
}
} }
.product-item {
.features {
margin: 2.4rem 0 1.6rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.6rem 2.4rem;
&-item {
padding-left: 1.6rem;
display: flex;
align-items: center;
background-color: var(--information-light);
border-radius: 100px;
color: var(--information-dark);
overflow: hidden;
p { margin: 0; }
button {
width: 3.2rem;
height: 3.2rem;
margin-left: 0.8rem;
display: flex;
align-items: center;
background: none;
border: none;
}
}
}
&-item {
--status-color: var(--gray-hard-darkest); --status-color: var(--gray-hard-darkest);
&.low { --status-color: var(--alert-light); } &.low { --status-color: var(--alert-light); }
&.out-of-stock { --status-color: var(--alert); } &.out-of-stock { --status-color: var(--alert); }
width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -91,9 +16,6 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest); background-color: var(--gray-soft-lightest);
&.out-of-stock { border-color: var(--status-color); } &.out-of-stock { border-color: var(--status-color); }
&:not(:first-child) {
margin-top: 1.6rem;
}
.itemInfo { .itemInfo {
min-width: 20ch; min-width: 20ch;

View File

@ -1,82 +1,30 @@
.products, .products,
.new-product, .new-product,
.edit-product { .edit-product {
max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding-bottom: 6rem; padding-bottom: 6rem;
@include grid-col(12);
.back-btn { gap: 3.2rem;
margin: 2.4rem 0;
padding: 0.4rem 0.8rem;
display: inline-flex;
align-items: center;
background-color: var(--gray-soft-darkest);
border-radius: var(--border-radius-sm);
color: var(--gray-soft-lightest);
i { margin-right: 0.8rem; }
&:hover {
background-color: var(--gray-hard-lightest);
cursor: pointer;
}
}
header { header {
padding: 2.4rem 0; @include header();
display: flex; grid-column: 1 / -1;
justify-content: space-between;
align-items: center;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
h2 {
margin: 0;
@include title-lg;
color: var(--gray-hard-darkest) !important;
}
h3 {
margin: 0;
@include text-lg(600);
color: var(--gray-hard-darkest) !important;
}
}
.layout {
display: flex;
align-items: flex-end;
gap: 0 3.2rem;
.span-7 { flex: 1 1 70%; }
.span-3 { flex: 1 1 30%; }
}
.main-action-btn {
background-color: var(--main);
color: var(--gray-soft-lightest);
border: none;
&:hover { opacity: 0.75; }
}
.main-actions {
display: flex;
justify-content: center;
align-items: center;
& > *:not(:first-child) {
margin-left: 1.6rem;
}
}
}
.products {
max-width: 1600px;
.layout {
align-items: flex-start;
} }
} }
.new-product, .new-product,
.edit-product { .edit-product {
max-width: 1300px;
padding-right: 1.6rem; &-nav {
padding-left: 1.6rem; max-width: 1600px;
margin: 0 auto;
@include grid-col(12);
justify-items: flex-start;
& > * {
grid-column: 2 / -2;
}
}
header { grid-column: 2 / -2; }
} }

View File

@ -1,7 +1,13 @@
.products-filters { .store-filters {
grid-column: 1 / 4;
padding-top: 1.6rem; padding-top: 1.6rem;
border-top: 1px solid var(--gray-soft-dark); border-top: 1px solid var(--gray-soft-dark);
header {
@include header();
padding: 0 0 2.4rem 0;
}
.accordion { .accordion {
&-item:not(:last-of-type) { &-item:not(:last-of-type) {
margin-bottom: 1.6rem; margin-bottom: 1.6rem;

View File

@ -0,0 +1,30 @@
.store-products-list {
grid-column: 4 / -1;
.features {
margin: 2.4rem 0 1.6rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.6rem 2.4rem;
&-item {
padding-left: 1.6rem;
display: flex;
align-items: center;
background-color: var(--information-light);
border-radius: 100px;
color: var(--information-dark);
overflow: hidden;
p { margin: 0; }
button {
width: 3.2rem;
height: 3.2rem;
margin-left: 0.8rem;
display: flex;
align-items: center;
background: none;
border: none;
}
}
}
}

View File

@ -1,170 +1,52 @@
.store {
margin: 0 auto;
padding-bottom: 6rem;
.back-btn {
margin: 2.4rem 0;
padding: 0.4rem 0.8rem;
display: inline-flex;
align-items: center;
background-color: var(--gray-soft-darkest);
border-radius: var(--border-radius-sm);
color: var(--gray-soft-lightest);
i { margin-right: 0.8rem; }
&:hover {
background-color: var(--gray-hard-lightest);
cursor: pointer;
}
}
header {
padding: 2.4rem 0;
display: flex;
justify-content: space-between;
align-items: center;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
h2 {
margin: 0;
@include title-lg;
color: var(--gray-hard-darkest) !important;
}
h3 {
margin: 0;
@include text-lg(600);
color: var(--gray-hard-darkest) !important;
}
}
.layout {
display: flex;
align-items: flex-end;
gap: 0 3.2rem;
.span-7 { flex: 1 1 70%; }
.span-3 { flex: 1 1 30%; }
}
.main-action-btn {
background-color: var(--main);
color: var(--gray-soft-lightest);
border: none;
&:hover { opacity: 0.75; }
}
.main-actions {
display: flex;
justify-content: center;
align-items: center;
& > *:not(:first-child) {
margin-left: 1.6rem;
}
}
}
.store { .store {
max-width: 1600px; max-width: 1600px;
@include grid-col(12);
.layout { gap: 3.2rem;
align-items: flex-start; margin: 0 auto;
} padding-bottom: 6rem;
&-filters { //&-product-item {
} // padding: 1rem 1.8rem;
// border: 1px solid var(--gray-soft-dark);
&-products-list { // border-radius: var(--border-radius);
.products { // background-color: var(--gray-soft-lightest);
display: flex;
flex-wrap: wrap; // margin-right: 1.6rem;
}
// .itemInfo-image {
.status { // align-items: center;
padding: 1.6rem 2.4rem;
display: flex; // img {
justify-content: space-between; // width: 19.8rem;
background-color: var(--gray-soft); // height: 14.8rem;
border-radius: var(--border-radius); // object-fit: cover;
p { margin: 0; } // border-radius: var(--border-radius);
.count { // background-color: var(--gray-soft);
p { // }
display: flex; // }
align-items: center; // .itemInfo-name {
@include text-sm; // margin: 1rem 0;
span { // @include text-base;
margin-left: 1.6rem; // font-weight: 600;
@include text-lg(600); // color: var(--gray-hard-darkest);
} // }
}
} // .actions {
} // display: flex;
.features { // align-items: center;
margin: 2.4rem 0 1.6rem; // .manage {
display: flex; // overflow: hidden;
flex-wrap: wrap; // display: flex;
align-items: center; // border-radius: var(--border-radius-sm);
gap: 1.6rem 2.4rem; // button {
&-item { // @include btn;
padding-left: 1.6rem; // border-radius: 0;
display: flex; // color: var(--gray-soft-lightest);
align-items: center; // &:hover { opacity: 0.75; }
background-color: var(--information-light); // }
border-radius: 100px; // .edit-btn {background: var(--gray-hard-darkest) }
color: var(--information-dark); // .delete-btn {background: var(--error) }
p { margin: 0; } // }
button { // }
width: 3.2rem; //}
height: 3.2rem;
background: none;
border: none;
}
}
}
}
&-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) }
}
}
}
} }

View File

@ -14,22 +14,12 @@
</div> </div>
</section> </section>
<section class="edit-product m-lg admin-store-manage"> <section class="admin-store-manage">
<div class="row"> <div class="edit-product-nav">
<a class="back-btn" ng-click="backProductsList()">
<div class="col-md-12"> <i class="fas fa-angle-left"></i>
<a class="back-btn" ng-click="backProductsList()"> <span translate>{{ 'app.admin.store.back_products_list' }}</span>
<i class="fas fa-angle-left"></i> </a>
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
</a>
</div>
</div> </div>
<div class="row"> <edit-product product-id="productId" on-success="onSuccess" on-error="onError"/>
</section>
<div>
<edit-product product-id="productId" on-success="onSuccess" on-error="onError"/>
</div>
</div>
</section>

View File

@ -14,22 +14,12 @@
</div> </div>
</section> </section>
<section class="new-product m-lg admin-store-manage"> <section class="admin-store-manage">
<div class="row"> <div class="new-product-nav">
<a class="back-btn" ng-click="backProductsList()" tabindex="0">
<div class="col-md-12"> <i class="fas fa-angle-left"></i>
<a class="back-btn" ng-click="backProductsList()" tabindex="0"> <span translate>{{ 'app.admin.store.back_products_list' }}</span>
<i class="fas fa-angle-left"></i> </a>
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
</a>
</div>
</div> </div>
<div class="row"> <new-product on-success="onSuccess" on-error="onError"/>
</section>
<div>
<new-product on-success="onSuccess" on-error="onError"/>
</div>
</div>
</section>

View File

@ -1942,15 +1942,16 @@ en:
filter_stock: "By stock status" filter_stock: "By stock status"
filter_stock_from: "From" filter_stock_from: "From"
filter_stock_to: "to" filter_stock_to: "to"
result_count: "Result count:"
display_options: "Display options:"
visible_only: "Visible products only"
sort: sort:
name_az: "A-Z" name_az: "A-Z"
name_za: "Z-A" name_za: "Z-A"
price_low: "Price: low to high" price_low: "Price: low to high"
price_high: "Price: high to low" price_high: "Price: high to low"
products_list: products_list_header:
result_count: "Result count:"
display_options: "Display options:"
visible_only: "Visible products only"
product_item:
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:

View File

@ -378,11 +378,22 @@ en:
store: store:
fablab_store: "FabLab Store" fablab_store: "FabLab Store"
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
store_product_item: products:
available: "Available" filter: "Filter"
limited_stock: "Limited stock" filter_clear: "Clear all"
out_of_stock: "Out of stock" filter_apply: "Apply"
add: "Add" 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"
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: