mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-19 08:52:25 +01:00
Cleanup files
This commit is contained in:
parent
857261ba62
commit
29993b0ec9
@ -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,8 +265,7 @@ 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}
|
||||||
@ -280,7 +279,6 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
|||||||
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>
|
||||||
|
|
||||||
|
100
app/frontend/src/javascript/components/store/product-item.tsx
Normal file
100
app/frontend/src/javascript/components/store/product-item.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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,8 +261,7 @@ 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'>
|
||||||
@ -309,36 +306,13 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='products-list span-7'>
|
<div className='store-products-list'>
|
||||||
<div className='status'>
|
<ProductsListHeader
|
||||||
<div className='count'>
|
productsCount={filteredProductsList.length}
|
||||||
<p>{t('app.admin.store.products.result_count')}<span>{filteredProductsList.length}</span></p>
|
selectOptions={buildOptions()}
|
||||||
</div>
|
onSelectOptionsChange={handleSorting}
|
||||||
<div className="display">
|
onSwitch={toggleVisible}
|
||||||
<div className='sort'>
|
|
||||||
<p>{t('app.admin.store.products.display_options')}</p>
|
|
||||||
<Select
|
|
||||||
options={buildOptions()}
|
|
||||||
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 className='features'>
|
<div className='features'>
|
||||||
{features.categories.map(c => (
|
{features.categories.map(c => (
|
||||||
<div key={c.id} className='features-item'>
|
<div key={c.id} className='features-item'>
|
||||||
@ -353,11 +327,16 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<ProductsList
|
|
||||||
products={filteredProductsList}
|
<div className="products-list">
|
||||||
|
{filteredProductsList.map((product) => (
|
||||||
|
<ProductItem
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
onEdit={editProduct}
|
onEdit={editProduct}
|
||||||
onDelete={deleteProduct}
|
onDelete={deleteProduct}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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,31 +71,105 @@ 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>Filtrer</h3>
|
<h3>{t('app.public.store.products.filter')}</h3>
|
||||||
<div className='grpBtn'>
|
<div className='grpBtn'>
|
||||||
<FabButton className="is-black">Clear</FabButton>
|
<FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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>
|
</div>
|
||||||
<div className='store-products-list span-7'>
|
<FabButton onClick={applyFilters} className="is-info">{t('app.public.store.products.filter_apply')}</FabButton>
|
||||||
<div className='status'>
|
|
||||||
<div className='count'>
|
|
||||||
<p>Result count: <span>{products.length}</span></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
</AccordionItem>
|
||||||
<div className='sort'>
|
<AccordionItem id={1}
|
||||||
<p>Display options:</p>
|
isOpen={accordion[1]}
|
||||||
</div>
|
onChange={handleAccordion}
|
||||||
<div className='visibility'>
|
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>
|
</div>
|
||||||
|
<FabButton onClick={applyFilters} className="is-info">{t('app.public.store.products.filter_apply')}</FabButton>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
</div>
|
</div>
|
||||||
</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'>
|
||||||
<div className='features-item'>
|
<div className='features-item'>
|
||||||
<p>feature name</p>
|
<p>feature name</p>
|
||||||
@ -82,17 +181,29 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="products">
|
<div className="products-grid">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} />
|
<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>
|
||||||
|
@ -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";
|
||||||
|
@ -12,3 +12,54 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
.manage-product-category {
|
|
||||||
|
|
||||||
}
|
|
@ -1,15 +1,14 @@
|
|||||||
.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 {
|
.create-button {
|
||||||
background-color: var(--gray-hard-darkest);
|
background-color: var(--gray-hard-darkest);
|
||||||
border-color: var(--gray-hard-darkest);
|
border-color: var(--gray-hard-darkest);
|
||||||
@ -20,21 +19,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h2 {
|
.fab-alert {
|
||||||
margin: 0;
|
grid-column: 2 / -2;
|
||||||
@include title-lg;
|
|
||||||
color: var(--gray-hard-darkest) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-action-btn {
|
|
||||||
background-color: var(--main);
|
|
||||||
color: var(--gray-soft-lightest);
|
|
||||||
border: none;
|
|
||||||
&:hover { opacity: 0.75; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tree {
|
&-tree {
|
||||||
|
grid-column: 2 / -2;
|
||||||
& > *:not(:first-child) {
|
& > *:not(:first-child) {
|
||||||
margin-top: 1.6rem;
|
margin-top: 1.6rem;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -17,22 +30,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.price-data-header {
|
||||||
@media (max-width: 1023px) {
|
@include grid-col(10);
|
||||||
.span-3,
|
gap: 3.2rem;
|
||||||
.span-7 {
|
|
||||||
flex-basis: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-data {
|
|
||||||
.layout {
|
|
||||||
align-items: center;
|
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,
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.products-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 3.2rem;
|
||||||
|
|
||||||
|
.store-product-item {
|
||||||
|
color: tomato;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +1,13 @@
|
|||||||
.products-list {
|
.products-list {
|
||||||
.status {
|
|
||||||
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) {
|
& > *:not(:first-child) {
|
||||||
margin-left: 2rem;
|
margin-top: 1.6rem;
|
||||||
padding-left: 2rem;
|
|
||||||
border-left: 1px solid var(--gray-hard-darkest);
|
|
||||||
}
|
}
|
||||||
|
.product-item {
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,170 +1,52 @@
|
|||||||
.store {
|
.store {
|
||||||
|
max-width: 1600px;
|
||||||
|
@include grid-col(12);
|
||||||
|
gap: 3.2rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-bottom: 6rem;
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
.back-btn {
|
//&-product-item {
|
||||||
margin: 2.4rem 0;
|
// padding: 1rem 1.8rem;
|
||||||
padding: 0.4rem 0.8rem;
|
// border: 1px solid var(--gray-soft-dark);
|
||||||
display: inline-flex;
|
// border-radius: var(--border-radius);
|
||||||
align-items: center;
|
// background-color: var(--gray-soft-lightest);
|
||||||
background-color: var(--gray-soft-darkest);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
color: var(--gray-soft-lightest);
|
|
||||||
i { margin-right: 0.8rem; }
|
|
||||||
|
|
||||||
&:hover {
|
// margin-right: 1.6rem;
|
||||||
background-color: var(--gray-hard-lightest);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
// .itemInfo-image {
|
||||||
padding: 2.4rem 0;
|
// align-items: center;
|
||||||
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 {
|
// img {
|
||||||
display: flex;
|
// width: 19.8rem;
|
||||||
align-items: flex-end;
|
// height: 14.8rem;
|
||||||
gap: 0 3.2rem;
|
// object-fit: cover;
|
||||||
.span-7 { flex: 1 1 70%; }
|
// border-radius: var(--border-radius);
|
||||||
.span-3 { flex: 1 1 30%; }
|
// background-color: var(--gray-soft);
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// .itemInfo-name {
|
||||||
|
// margin: 1rem 0;
|
||||||
|
// @include text-base;
|
||||||
|
// font-weight: 600;
|
||||||
|
// color: var(--gray-hard-darkest);
|
||||||
|
// }
|
||||||
|
|
||||||
.main-action-btn {
|
// .actions {
|
||||||
background-color: var(--main);
|
// display: flex;
|
||||||
color: var(--gray-soft-lightest);
|
// align-items: center;
|
||||||
border: none;
|
// .manage {
|
||||||
&:hover { opacity: 0.75; }
|
// overflow: hidden;
|
||||||
}
|
// display: flex;
|
||||||
|
// border-radius: var(--border-radius-sm);
|
||||||
.main-actions {
|
// button {
|
||||||
display: flex;
|
// @include btn;
|
||||||
justify-content: center;
|
// border-radius: 0;
|
||||||
align-items: center;
|
// color: var(--gray-soft-lightest);
|
||||||
& > *:not(:first-child) {
|
// &:hover { opacity: 0.75; }
|
||||||
margin-left: 1.6rem;
|
// }
|
||||||
}
|
// .edit-btn {background: var(--gray-hard-darkest) }
|
||||||
}
|
// .delete-btn {background: var(--error) }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
.store {
|
//}
|
||||||
max-width: 1600px;
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-filters {
|
|
||||||
}
|
|
||||||
|
|
||||||
&-products-list {
|
|
||||||
.products {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: 1.6rem 2.4rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
background-color: var(--gray-soft);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
p { margin: 0; }
|
|
||||||
.count {
|
|
||||||
p {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
@include text-sm;
|
|
||||||
span {
|
|
||||||
margin-left: 1.6rem;
|
|
||||||
@include text-lg(600);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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);
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
|
||||||
<div class="col-md-12">
|
|
||||||
<a class="back-btn" ng-click="backProductsList()">
|
<a class="back-btn" ng-click="backProductsList()">
|
||||||
<i class="fas fa-angle-left"></i>
|
<i class="fas fa-angle-left"></i>
|
||||||
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
|
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<edit-product product-id="productId" on-success="onSuccess" on-error="onError"/>
|
<edit-product product-id="productId" on-success="onSuccess" on-error="onError"/>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
@ -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">
|
||||||
|
|
||||||
<div class="col-md-12">
|
|
||||||
<a class="back-btn" ng-click="backProductsList()" tabindex="0">
|
<a class="back-btn" ng-click="backProductsList()" tabindex="0">
|
||||||
<i class="fas fa-angle-left"></i>
|
<i class="fas fa-angle-left"></i>
|
||||||
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
|
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<new-product on-success="onSuccess" on-error="onError"/>
|
<new-product on-success="onSuccess" on-error="onError"/>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user