From 58402db25529f56991265b2feea4290c965e96da Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Tue, 18 Jan 2022 19:22:56 +0300 Subject: [PATCH 001/133] working --- components/VariantPicker.tsx | 51 ------- components/header/ChooseVariantModal.tsx | 4 +- components/product/Images.tsx | 55 +++++++ components/product/Labels.tsx | 5 +- components/product/PriceAndBuy.tsx | 127 ++++++++-------- components/product/ProductImages.tsx | 53 ------- components/product/VariantAndBuy.tsx | 17 +++ components/product/VariantPicker.tsx | 67 +++++++++ .../product/variantPicker/Characteristic.tsx | 87 +++++++++++ components/productsList/ProductItem.tsx | 5 +- components/variantPicker/Characteristic.tsx | 77 ---------- lib/product.ts | 19 +++ pages/product/[slug].tsx | 81 +++++----- styles/components/pageHeader.scss | 5 + styles/components/product.scss | 141 ++---------------- styles/components/product/gallery.scss | 39 +++++ styles/components/product/labels.scss | 21 ++- styles/components/product/variantPicker.scss | 3 + styles/components/products.scss | 9 +- styles/themes/sample/page.scss | 6 - yarn.lock | 2 +- 21 files changed, 437 insertions(+), 437 deletions(-) delete mode 100644 components/VariantPicker.tsx create mode 100644 components/product/Images.tsx delete mode 100644 components/product/ProductImages.tsx create mode 100644 components/product/VariantAndBuy.tsx create mode 100644 components/product/VariantPicker.tsx create mode 100644 components/product/variantPicker/Characteristic.tsx delete mode 100644 components/variantPicker/Characteristic.tsx create mode 100644 lib/product.ts create mode 100644 styles/components/pageHeader.scss create mode 100644 styles/components/product/gallery.scss create mode 100644 styles/components/product/variantPicker.scss diff --git a/components/VariantPicker.tsx b/components/VariantPicker.tsx deleted file mode 100644 index c87287c..0000000 --- a/components/VariantPicker.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import {IExtendedVariants, IProductVariant} from 'boundless-api-client'; -import {useState} from 'react'; -import VariantCharacteristic from './variantPicker/Characteristic'; - -export default function VariantPicker({variants, onPick, error, setError}: IVariantPickerProps) { - const {characteristics, idCombinations, list} = variants; - const [values, setValues] = useState<{[key: number]: number}>({}); - - const variantSelected = (charId: number, caseId: number) => { - const newValues = {...values, [charId]: caseId}; - setValues(newValues); - setError(false); - - const filteredList = list.filter(variant => { - let out = true; - for (const [key, value] of Object.entries(newValues)) { - out = out && idCombinations[variant.variant_id][key] === value; - } - return out; - }); - - onPick(filteredList); - }; - - return ( - <> -
- {characteristics.map(characteristic => ( -
- -
- ))} - {error &&
- Please pick a product variant -
} -
- - ); -} - -interface IVariantPickerProps { - variants: IExtendedVariants; - onPick: (variant: IProductVariant[]) => void; - error: boolean; - setError: (value: boolean) => void; -} \ No newline at end of file diff --git a/components/header/ChooseVariantModal.tsx b/components/header/ChooseVariantModal.tsx index aca6ab0..3baf2df 100644 --- a/components/header/ChooseVariantModal.tsx +++ b/components/header/ChooseVariantModal.tsx @@ -6,7 +6,7 @@ import {getLowestPrice} from '../../lib/services/price'; import {addItem2Cart} from '../../redux/actions/cart'; import {hideVariantModal} from '../../redux/reducers/cart'; import {RootState} from '../../redux/store'; -import VariantPicker from '../VariantPicker'; +import ProductVariantPicker from '../product/VariantPicker'; export default function ChooseVariantModal() { const dispatch = useAppDispatch(); @@ -46,7 +46,7 @@ export default function ChooseVariantModal() {
Please select a variant:
- + {variantPicked &&

{!variantPicked && From: } diff --git a/components/product/Images.tsx b/components/product/Images.tsx new file mode 100644 index 0000000..8fe058a --- /dev/null +++ b/components/product/Images.tsx @@ -0,0 +1,55 @@ +import React, {useState} from 'react'; +import {IProductImage} from 'boundless-api-client/types/image'; +import clsx from 'clsx'; +import ProductImage from './ProductImage'; +import {getMetaImgUrl, productImgRatio} from '../../lib/services/imgs'; +import ImagesSlider from './ImagesSlider'; +import NoImage from '../NoImage'; +import {IProductItem} from 'boundless-api-client'; + +export default function ProductImages({product}: {product: IProductItem}) { + const [activeImg, setActiveImg] = useState(0); + + const onThumbClick = (e: React.MouseEvent, index: number) => { + e.preventDefault(); + setActiveImg(index); + }; + + const images = product.images; + if (!images || !images.length) + return ; + + return ( + <> +

+ +
+ e.preventDefault()}> + + + +
+ {images[activeImg].description!} +
+
+
+ +
+ +
+ + ); +} \ No newline at end of file diff --git a/components/product/Labels.tsx b/components/product/Labels.tsx index 160705d..a8aa01f 100644 --- a/components/product/Labels.tsx +++ b/components/product/Labels.tsx @@ -6,13 +6,14 @@ import {faFire} from '@fortawesome/free-solid-svg-icons/faFire'; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faTag} from '@fortawesome/free-solid-svg-icons/faTag'; import {faHeart} from '@fortawesome/free-solid-svg-icons/faHeart'; +import clsx from 'clsx'; -export default function ProductLabels({labels}: {labels: ILabel[]}) { +export default function ProductLabels({labels, className}: {labels: ILabel[], className?: string}) { if (!labels.length) return null; return ( -
    +
      {labels.map(({label_id, title, color, text_color, icon}) =>
    • 0; + // const [variant] = selectedVariants; + // const inStock = hasVariants ? variant?.in_stock || false : product.in_stock || false; + // const [qty, setQty] = useState(1); + // + // const onSubmit = () => { + // if ((hasVariants && !variantPicked) || !inStock) { + // setError(true); + // } else { + // dispatch(addItem2Cart(hasVariants ? variant.item_id : product.item_id, qty)); + // } + // }; - const variantPicked = selectedVariants.length === 1; - const isVariantAvailable = selectedVariants.length > 0; - const [variant] = selectedVariants; - const inStock = hasVariants ? variant?.in_stock || false : product.in_stock || false; - const [qty, setQty] = useState(1); - - const onSubmit = () => { - if ((hasVariants && !variantPicked) || !inStock) { - setError(true); - } else { - dispatch(addItem2Cart(hasVariants ? variant.item_id : product.item_id, qty)); - } - }; + const price = getPriceForTpl(product.price, product.old_price); return (
      - {isVariantAvailable || !hasVariants ? <> -

      - {!variantPicked && hasVariants && From: } - {price.price} - {price.price_old && {price.price_old}} -

      - {price.benefit &&

      - - {price.benefit} -

      } - {(variantPicked || !hasVariants) &&

      - {inStock ? 'In stock' : 'Out of stock'} -

      } - {inStock && -
      -
      - - setQty(Number(e.target.value) || 0)} - /> - -
      - -
      } - - :
      Combination not available.
      } -
      + {/*

      */} + {/* {!variantPicked && hasVariants && From: }*/} + {/* {price.price}*/} + {/* {price.price_old && {price.price_old}}*/} + {/*

      */} + {/*{price.benefit &&

      */} + {/* */} + {/* {price.benefit}*/} + {/*

      }*/} + {/*{(variantPicked || !hasVariants) &&

      */} + {/* {inStock ? 'In stock' : 'Out of stock'}*/} + {/*

      }*/} + {/*{inStock &&*/} + {/*
      */} + {/*
      */} + {/* setQty(qty - 1)}*/} + {/* ><>–*/} + {/* setQty(Number(e.target.value) || 0)}*/} + {/* />*/} + {/* setQty(qty + 1)}*/} + {/* >+*/} + {/*
      */} + {/* */} + {/*
      }*/} +
); } interface IPriceAndBuyProps { - selectedVariants: IProductVariant[] | []; - setError: (value: boolean) => void; + // selectedVariants: IProductVariant[] | []; + // setError: (value: boolean) => void; product: IProductItem; + selectedVariant?: IProductVariant } \ No newline at end of file diff --git a/components/product/ProductImages.tsx b/components/product/ProductImages.tsx deleted file mode 100644 index 29b05da..0000000 --- a/components/product/ProductImages.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, {useState} from 'react'; -import {IProductImage} from 'boundless-api-client/types/image'; -import clsx from 'clsx'; -import ProductImage from './ProductImage'; -import {getMetaImgUrl} from '../../lib/services/imgs'; -import ImagesSlider from './ImagesSlider'; - -export default function ProductImages({images}: {images: IProductImage[]}) { - const [activeImg, setActiveImg] = useState(0); - - const onThumbClick = (e: React.MouseEvent, index: number) => { - e.preventDefault(); - setActiveImg(index); - }; - - return ( -
- {images.length > 0 ? - <> -
- -
- e.preventDefault()}> - - - -
- {images[activeImg].description!} -
-
-
- -
- -
- - :
Product has no images
} -
- ); -} \ No newline at end of file diff --git a/components/product/VariantAndBuy.tsx b/components/product/VariantAndBuy.tsx new file mode 100644 index 0000000..721f46a --- /dev/null +++ b/components/product/VariantAndBuy.tsx @@ -0,0 +1,17 @@ +import {IProductItem} from 'boundless-api-client'; +import ProductVariantPicker from './VariantPicker'; +import ProductPriceAndBuy from './PriceAndBuy'; + + +export default function ProductVariantAndBuy({product}: {product: IProductItem}) { + const onCaseChange = () => {}; + + return ( + <> + {product.has_variants && } + + + ); +} \ No newline at end of file diff --git a/components/product/VariantPicker.tsx b/components/product/VariantPicker.tsx new file mode 100644 index 0000000..16418b9 --- /dev/null +++ b/components/product/VariantPicker.tsx @@ -0,0 +1,67 @@ +import {IExtendedVariants, IProductVariant} from 'boundless-api-client'; +import {useState} from 'react'; +import VariantPickerCharacteristic from './variantPicker/Characteristic'; +import {IVariantCombination} from 'boundless-api-client/src/types/catalog/variant'; +import _isEqual from 'lodash/isEqual'; + +export default function ProductVariantPicker({extendedVariants, onChange}: IVariantPickerProps) { + const {characteristics, list, combinations} = extendedVariants; + const [value, setValue] = useState<{[characteristicId: number]: number}>({}); + + const onSelectCase = (characteristicId: number, caseId: number|null) => { + const newValue = {...value}; + if (caseId === null) { + delete newValue[characteristicId]; + } else { + newValue[characteristicId] = caseId; + } + + setValue(newValue); + + let variant: IProductVariant|undefined; + const variantId = findVariantIdByCombinations(newValue, combinations); + if (variantId) { + variant = list.find(({variant_id}) => String(variant_id) == variantId); + } + + if (onChange) { + onChange(newValue, variant); + } + }; + + return ( +
+ {characteristics.map(characteristic => ( + + // d-flex gap-2 mb-3 align-items-center + //
+ //
+ ))} + {/*{error &&
*/} + {/* Please pick a product variant*/} + {/*
}*/} +
+ ); +} + +interface IVariantPickerProps { + extendedVariants: IExtendedVariants; + onChange?: (value: {[characteristicId: number]: number}, variant?: IProductVariant) => void +} + +const findVariantIdByCombinations = (value: {[key: number]: number}, combinations: IVariantCombination): null|string => { + const requiredCombinations = Object.entries(value).map(([characteristicId, caseId]) => `${characteristicId}-${caseId}`); + + const result = Object.entries(combinations).find(([variantId, variantCombination]) => { + return _isEqual(requiredCombinations, variantCombination); + }); + + return result ? result[0] : null; +}; \ No newline at end of file diff --git a/components/product/variantPicker/Characteristic.tsx b/components/product/variantPicker/Characteristic.tsx new file mode 100644 index 0000000..965942a --- /dev/null +++ b/components/product/variantPicker/Characteristic.tsx @@ -0,0 +1,87 @@ +import {IVariantCharacteristic, IVariantIdCombinations} from 'boundless-api-client'; +import {ChangeEvent, useEffect, useRef} from 'react'; + +// values, onSelect, idCombinations +export default function VariantPickerCharacteristic({characteristic, onSelectCase, value}: IVariantCharProps) { + /* + const value = values[characteristic.id]; + const submitted = useRef(false); + + const checkAvailable = (caseId: number) => { + const _values = {...values}; + delete _values[characteristic.id]; + if (!Object.keys(_values).length) return true; + + for (const combination of Object.values(idCombinations)) { + let out = true; + for (const [key, value] of Object.entries(_values)) { + out = out && combination[key] === value; + } + if (out && combination[characteristic.id] === caseId) return true; + } + + return false; + }; + + const checkRowAvailability = () => { + const available = characteristic.cases.filter(caseItem => checkAvailable(caseItem.id)); + + if (available.length === 1) { + onSubmit(available[0].id); + } + }; + + const onSubmit = (id: number) => { + submitted.current = true; + onSelect(characteristic.id, id); + }; + + useEffect(() => { + if (!submitted.current) { + checkRowAvailability(); + } else { + submitted.current = false; + } + }, [values, characteristic]); //eslint-disable-line +*/ + + const onInputChange = (caseId: number, e: ChangeEvent) => { + onSelectCase(characteristic.id, e.currentTarget.checked ? caseId : null); + }; + + return ( +
+
{`${characteristic.title}:`}
+
+ {characteristic.cases.map(caseItem => { + const id = `${characteristic.id}-case-${caseItem.id}`; + + return ( +
+ + +
+ ); + })} +
+
+ ); +} + +interface IVariantCharProps { + characteristic: IVariantCharacteristic; + value: {[key: number]: number}; + onSelectCase: (characteristicId: number, caseId: number|null) => void; + // idCombinations: IVariantIdCombinations; +} \ No newline at end of file diff --git a/components/productsList/ProductItem.tsx b/components/productsList/ProductItem.tsx index 0604038..1a84676 100644 --- a/components/productsList/ProductItem.tsx +++ b/components/productsList/ProductItem.tsx @@ -78,7 +78,10 @@ function ProductImage({product, productUrl}: {product: IProduct, productUrl: str ? : } - + ); diff --git a/components/variantPicker/Characteristic.tsx b/components/variantPicker/Characteristic.tsx deleted file mode 100644 index 3c3f28f..0000000 --- a/components/variantPicker/Characteristic.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import {IVariantCharacteristic, IVariantIdCombinations} from 'boundless-api-client'; -import {useEffect, useRef} from 'react'; - -export default function VariantCharacteristic({characteristic, values, onSelect, idCombinations}: IVariantCharProps) { - const value = values[characteristic.id]; - const submitted = useRef(false); - - const checkAvailable = (caseId: number) => { - const _values = {...values}; - delete _values[characteristic.id]; - if (!Object.keys(_values).length) return true; - - for (const combination of Object.values(idCombinations)) { - let out = true; - for (const [key, value] of Object.entries(_values)) { - out = out && combination[key] === value; - } - if (out && combination[characteristic.id] === caseId) return true; - } - - return false; - }; - - const checkRowAvailability = () => { - const available = characteristic.cases.filter(caseItem => checkAvailable(caseItem.id)); - - if (available.length === 1) { - onSubmit(available[0].id); - } - }; - - const onSubmit = (id: number) => { - submitted.current = true; - onSelect(characteristic.id, id); - }; - - useEffect(() => { - if (!submitted.current) { - checkRowAvailability(); - } else { - submitted.current = false; - } - }, [values, characteristic]); //eslint-disable-line - - return ( - <> -
{`${characteristic.title}: `}
-
- {characteristic.cases.map(caseItem => ( -
- onSubmit(caseItem.id)} - type='radio' - checked={value === caseItem.id} - value={caseItem.id} - id={'case-' + caseItem.id} - /> - -
- ))} -
- - ); -} - -interface IVariantCharProps { - characteristic: IVariantCharacteristic; - values: {[key: number]: number}; - onSelect: (charId: number, caseId: number) => void; - idCombinations: IVariantIdCombinations; -} \ No newline at end of file diff --git a/lib/product.ts b/lib/product.ts new file mode 100644 index 0000000..a1ec12d --- /dev/null +++ b/lib/product.ts @@ -0,0 +1,19 @@ +export interface IPriceForTpl { + price: number, + oldPrice: number|null, + isFrom: boolean +} + +export const getPriceForTpl = (curPrice: number|number[], oldPrice: null|number|number[]): IPriceForTpl => { + const out: IPriceForTpl = { + price: Array.isArray(curPrice) ? curPrice[0] : curPrice, + oldPrice: null, + isFrom: (Array.isArray(curPrice) && curPrice[0] != curPrice[1]) ? true : false + }; + + if (oldPrice && !(Array.isArray(curPrice) && !Array.isArray(oldPrice))) { + out.oldPrice = Array.isArray(oldPrice) ? oldPrice[0] : oldPrice; + } + + return out; +}; \ No newline at end of file diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index fec24ed..ea7a870 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -1,71 +1,68 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useMemo, useState} from 'react'; import {ICategoryFlatItem, IProductItem, IProductVariant} from 'boundless-api-client'; import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next'; import MainLayout from '../../layouts/Main'; import {apiClient} from '../../lib/services/api'; import {useRouter} from 'next/router'; import BreadCrumbs from '../../components/BreadCrumbs'; -import ProductImages from '../../components/product/ProductImages'; -import VariantPicker from '../../components/VariantPicker'; -import PriceAndBuy from '../../components/product/PriceAndBuy'; -import qs from 'qs'; +import ProductImages from '../../components/product/Images'; +import ProductVariantPicker from '../../components/product/VariantPicker'; +import ProductPriceAndBuy from '../../components/product/PriceAndBuy'; +import qs, {ParsedQs} from 'qs'; import ProductMeta from '../../components/product/ProductMeta'; import ProductCharacteristics from '../../components/product/ProductCharacteristics'; import {getProductMetaData} from '../../lib/services/meta'; +import ProductLabels from '../../components/product/Labels'; +import ProductVariantAndBuy from '../../components/product/VariantAndBuy'; export default function ProductPage({data}: InferGetStaticPropsType) { - const router = useRouter(); - const {product, categoryParents} = data!; - const [parents, setParents] = useState(categoryParents); - const [selectedVariants, setSelectedVariants] = useState(product?.extendedVariants?.list || []); + const {product, categoryParents} = data; + const [resolvedParents, setResolvedParents] = useState(categoryParents); + const [selectedVariants, setSelectedVariants] = useState(product.extendedVariants?.list || []); const [error, setError] = useState(false); - const title = product?.text.custom_title || product?.text.title; - const query = qs.parse(router.asPath.split('?')[1] || ''); - const {category, ...requestParams} = query; + const router = useRouter(); + const query = useMemo(() => qs.parse(router.asPath.split('?')[1] || ''), [router.asPath]); + const {category, ...restQuery} = query; - const fetchNewParents = async (categoryId: number) => { - const {parents} = await apiClient.catalog.getCategoryItem(categoryId, {with_parents: 1}); - setParents(parents!); - }; + const fetchParents = async (categoryId: number) => + setResolvedParents(await apiClient.catalog.getCategoryParents(categoryId)); useEffect(() => { - if (!product) return; - const categoryId = parseInt(category as string) || null; + const categoryId = category ? parseInt(category as string) : null; if (!categoryId) return; + const notDefaultCat = product.categoryRels.some(cat => (cat.is_default !== true && cat.category_id === categoryId)); if (notDefaultCat) { - fetchNewParents(categoryId); + fetchParents(categoryId); } - }, []); //eslint-disable-line + }, [category, product]); return ( <> - -
- {parents && } -
+ +
+ {resolvedParents && } +
-

{product?.text.title}

- +

{product.text.title}

+ +
- {product?.has_variants && - <> - - } - - -

Shipping

- We ship ASAP! + + {/**/} + {/**/} + {/*

Shipping

*/} + {/*We ship ASAP!*/}
-
- {product?.text.description &&
} -
- + {product.text.description &&
} +
@@ -102,7 +99,6 @@ export const getStaticProps: GetStaticProps = async ({params} } catch (error: any) { if (error.response?.status === 404) { return { - props: {}, notFound: true }; } else { @@ -132,8 +128,7 @@ const fetchData = async (slug: string) => { const categoryId = product.categoryRels.find(cat => cat.is_default === true)?.category_id; let categoryParents = null; if (categoryId) { - const {parents} = await apiClient.catalog.getCategoryItem(categoryId, {with_parents: 1}); - categoryParents = parents!; + categoryParents = await apiClient.catalog.getCategoryParents(categoryId); } return { @@ -144,10 +139,10 @@ const fetchData = async (slug: string) => { interface IProductPageProps { - data: IProductPageData | null; + data: IProductPageData; } interface IProductPageData { - product: IProductItem | null; + product: IProductItem; categoryParents: ICategoryFlatItem[] | null; } diff --git a/styles/components/pageHeader.scss b/styles/components/pageHeader.scss new file mode 100644 index 0000000..3672077 --- /dev/null +++ b/styles/components/pageHeader.scss @@ -0,0 +1,5 @@ +.page-header { + &_h1 { + + } +} diff --git a/styles/components/product.scss b/styles/components/product.scss index 266ff3c..bed6494 100644 --- a/styles/components/product.scss +++ b/styles/components/product.scss @@ -1,136 +1,20 @@ @import './product/labels'; +@import './product/gallery'; +@import './product/variantPicker'; -.product-item { - display: flex; - flex-direction: column; - margin-bottom: 16px; - - &__wrapper { - // border: 1px solid #eee; - border-radius: 10px; - box-shadow: 0 6px 24px -4px #eee; - display: flex; - flex-direction: column; - flex-grow: 1; - overflow: hidden; - padding: 10px; - position: relative; - - &:hover { - box-shadow: 0 6px 24px -4px #ccc; - - .product-item__stock-out { - display: block; - } - - .product-item__availability, - .product-item__basket-btn { - transform: translateY(0); - } - } - } - - &__title { - font-size: 1.3rem; - font-weight: 300; - margin-bottom: 0; - padding: 12px 6px; - text-align: center; - - a { - color: #ba86da; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - } - - &__price { - font-weight: 500; - text-align: center; - } - - &__image { - &.ratio-2-3 { - padding-bottom: 66.666%; - position: relative; - width: 100%; - } - - img { - height: 100%; - width: 100%; - } - - //.no-image { - // align-items: center; - // background: #f7f7f7; - // display: flex; - // height: 200px; - // justify-content: center; - // - // img { - // height: 80px; - // width: 80px; - // } - //} - } - - &__stock-in { - display: none; - } - - &__stock-out { - display: none; - } - - &__availability, - &__basket-btn { - background-color: #fff; - bottom: 0; - display: flex; - justify-content: center; - left: 0; - margin-bottom: 5px; - padding: 5px 10px; - position: absolute; - right: 0; - transform: translateY(60px); - transition: transform 0.3s; - } -} - -.thumbs-list .thumb { - border: 2px solid transparent; - border-radius: 8px; - overflow: hidden; - - a { - display: block; - opacity: 0.3; - } - - &.active { - border: 2px solid #ddd; - border-radius: 8px; - overflow: hidden; - - a { - opacity: 1; - } - } -} - -.variant-picker { - padding-bottom: 12px; +.product-description { + max-width: 800px; } -.variant-picker + .price-and-buy { - border-top: 1px solid #eee; -} +//.variant-picker { +// padding-bottom: 12px; +//} +// +//.variant-picker + .price-and-buy { +// border-top: 1px solid #eee; +//} +/* .price-and-buy { border-bottom: 1px solid #eee; margin-bottom: 20px; @@ -155,6 +39,7 @@ color: $front-highlighted-price-color; } } + */ .modal-price { .prices { diff --git a/styles/components/product/gallery.scss b/styles/components/product/gallery.scss new file mode 100644 index 0000000..e220313 --- /dev/null +++ b/styles/components/product/gallery.scss @@ -0,0 +1,39 @@ +.product-gallery { + display: flex; + + &__thumbs { + flex-shrink: 0; + } + + &__thumb { + margin: 0 0 10px; + + &.active { + .product-gallery__thumb-link { + border-color: $btn-action-border; + opacity: 1; + } + } + } + + &__thumb-link { + border: 2px solid transparent; + cursor: pointer; + display: flex; + opacity: 0.3; + padding: 4px; + transition: 0.3s ease 0s; + + img { + display: block; + height: auto; + max-width: 100%; + } + } + + &__big-img { + flex-grow: 1; + margin: 0 40px; + text-align: center; + } +} diff --git a/styles/components/product/labels.scss b/styles/components/product/labels.scss index 0b430e3..26755af 100644 --- a/styles/components/product/labels.scss +++ b/styles/components/product/labels.scss @@ -1,18 +1,29 @@ .product { &__labels { - align-items: flex-start; display: flex; - flex-direction: column; justify-content: flex-start; margin: 0; padding: 0; - position: absolute; + //position: absolute; + + &_column { + align-items: flex-start; + flex-direction: column; + } + + &_small { + .product__label { + font-size: 0.75rem; + padding: 5px; + } + } } &__label { - border-radius: 4px; display: inline-block; + margin: 10px; - padding: 4px 10px 5px; + //padding: 4px 10px 5px; + padding: 5px 8px; } } diff --git a/styles/components/product/variantPicker.scss b/styles/components/product/variantPicker.scss new file mode 100644 index 0000000..06d9507 --- /dev/null +++ b/styles/components/product/variantPicker.scss @@ -0,0 +1,3 @@ +.variant-picker { + +} \ No newline at end of file diff --git a/styles/components/products.scss b/styles/components/products.scss index 0abd9dc..1f9b9c8 100644 --- a/styles/components/products.scss +++ b/styles/components/products.scss @@ -103,19 +103,18 @@ $products-gutter: 10px; } } - .product__labels { left: 10px; + position: absolute; top: 10px; } .product__label { border-radius: 4px; - font-size: 0.75rem; - padding: 5px; + margin: 0 0 6px; - &:not(:last-child) { - margin-bottom: 6px; + &:last-child { + margin-bottom: 0; } } } diff --git a/styles/themes/sample/page.scss b/styles/themes/sample/page.scss index 3c2e889..e1a8068 100644 --- a/styles/themes/sample/page.scss +++ b/styles/themes/sample/page.scss @@ -4,12 +4,6 @@ body .page { min-height: 100vh; } -body .content-box { - background: #fff; - border-radius: 10px; - padding: 10px; -} - body h2 { color: #ba86da; } diff --git a/yarn.lock b/yarn.lock index 61dd35e..2b7f53e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -665,7 +665,7 @@ bootstrap@^5.1.3: "boundless-api-client@ssh://git@github.com:kirill-zhirnov/boundless-api-client": version "0.0.1" - resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#f78b72ed9f490fbbeb3ddddf4b75c3d38d98344a" + resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#36b455cbd0c6fda492f882881b2948f69406e61b" dependencies: axios "^0.24.0" jsonwebtoken "^8.5.1" From 0a96832f104e035bbb516b425826b1c8133a55df Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Tue, 18 Jan 2022 19:23:22 +0300 Subject: [PATCH 002/133] working --- components/FilterForm.tsx | 2 +- components/cart/CartItems.tsx | 2 +- lib/{services => }/api.ts | 0 lib/services/imgs.ts | 2 +- pages/cart.tsx | 2 +- pages/category/[slug].tsx | 2 +- pages/index.tsx | 2 +- pages/product/[slug].tsx | 2 +- redux/actions/cart.ts | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename lib/{services => }/api.ts (100%) diff --git a/components/FilterForm.tsx b/components/FilterForm.tsx index 7d1ee76..c795344 100644 --- a/components/FilterForm.tsx +++ b/components/FilterForm.tsx @@ -1,7 +1,7 @@ import {IFilterField, IFilterFieldRange, TCharacteristicType, TFilterFieldType} from 'boundless-api-client'; import {TQuery} from '../@types/common'; import {SyntheticEvent, useCallback, useEffect, useState} from 'react'; -import {apiClient} from '../lib/services/api'; +import {apiClient} from '../lib/api'; import PriceRangeField from './filterForm/PriceRange'; import _debounce from 'lodash/debounce'; import _omit from 'lodash/omit'; diff --git a/components/cart/CartItems.tsx b/components/cart/CartItems.tsx index 0422508..efef287 100644 --- a/components/cart/CartItems.tsx +++ b/components/cart/CartItems.tsx @@ -2,7 +2,7 @@ import {ICartItem} from 'boundless-api-client'; import {useEffect, useMemo, useRef, useState} from 'react'; import {useAppDispatch, useAppSelector} from '../../hooks/redux'; import {calcTotal, calcTotalPrice} from '../../lib/calculator'; -import {apiClient} from '../../lib/services/api'; +import {apiClient} from '../../lib/api'; import {addPromise} from '../../redux/reducers/xhr'; import {RootState} from '../../redux/store'; import debounce from 'lodash/debounce'; diff --git a/lib/services/api.ts b/lib/api.ts similarity index 100% rename from lib/services/api.ts rename to lib/api.ts diff --git a/lib/services/imgs.ts b/lib/services/imgs.ts index a44d632..8bf240c 100644 --- a/lib/services/imgs.ts +++ b/lib/services/imgs.ts @@ -1,5 +1,5 @@ import {TThumbRatio} from '../../@types/image'; -import {apiClient} from './api'; +import {apiClient} from '../api'; //fixed aspect ratio for product images export const productImgRatio = process.env.BOUNDLESS_PRODUCTS_IMAGE_PROPORTION as TThumbRatio || null; diff --git a/pages/cart.tsx b/pages/cart.tsx index 33d7247..06c17f6 100644 --- a/pages/cart.tsx +++ b/pages/cart.tsx @@ -3,7 +3,7 @@ import {useEffect, useState} from 'react'; import CartItems from '../components/cart/CartItems'; import {useAppDispatch, useAppSelector} from '../hooks/redux'; import MainLayout from '../layouts/Main'; -import {apiClient} from '../lib/services/api'; +import {apiClient} from '../lib/api'; import {setCartTotal} from '../redux/reducers/cart'; import {addPromise} from '../redux/reducers/xhr'; import {RootState} from '../redux/store'; diff --git a/pages/category/[slug].tsx b/pages/category/[slug].tsx index bbdcdce..e4842d7 100644 --- a/pages/category/[slug].tsx +++ b/pages/category/[slug].tsx @@ -1,6 +1,6 @@ import {useEffect, useState} from 'react'; import MainLayout from '../../layouts/Main'; -import {apiClient} from '../../lib/services/api'; +import {apiClient} from '../../lib/api'; import {GetServerSideProps, InferGetServerSidePropsType} from 'next'; import {ICategoryItem} from 'boundless-api-client/types/catalog/category'; import ErrorComponent from 'next/error'; diff --git a/pages/index.tsx b/pages/index.tsx index 49bad91..3b15ee4 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -4,7 +4,7 @@ import {GetServerSideProps, InferGetServerSidePropsType} from 'next'; import CategoryHomeMenu from '../components/category/CategoryHomeMenu'; import ProductsList from '../components/ProductsList'; import MainLayout from '../layouts/Main'; -import {apiClient} from '../lib/services/api'; +import {apiClient} from '../lib/api'; export default function IndexPage({categoryTree, products}: InferGetServerSidePropsType) { diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index ea7a870..ebf7331 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -2,7 +2,7 @@ import {useEffect, useMemo, useState} from 'react'; import {ICategoryFlatItem, IProductItem, IProductVariant} from 'boundless-api-client'; import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next'; import MainLayout from '../../layouts/Main'; -import {apiClient} from '../../lib/services/api'; +import {apiClient} from '../../lib/api'; import {useRouter} from 'next/router'; import BreadCrumbs from '../../components/BreadCrumbs'; import ProductImages from '../../components/product/Images'; diff --git a/redux/actions/cart.ts b/redux/actions/cart.ts index 4b912b5..4bbb09c 100644 --- a/redux/actions/cart.ts +++ b/redux/actions/cart.ts @@ -1,7 +1,7 @@ import {setCartId, setCartLoading, setCartSubmitting, setCartTotal, showCall2Order, showVariantModal} from '../reducers/cart'; import {AppThunk} from '../store'; import Cookie from 'js-cookie'; -import {apiClient} from '../../lib/services/api'; +import {apiClient} from '../../lib/api'; import {addPromise} from '../reducers/xhr'; import {showErrorAlert} from '../reducers/alert'; From 3075473f9aa6f73c6e7ec223318093a5fd5fc19c Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Tue, 18 Jan 2022 19:24:08 +0300 Subject: [PATCH 003/133] working --- components/BreadCrumbs.tsx | 2 +- components/FilterForm.tsx | 2 +- components/cart/CartRow.tsx | 4 ++-- components/category/CategoryHomeMenu.tsx | 4 ++-- components/category/Sidebar.tsx | 4 ++-- components/header/ChooseVariantModal.tsx | 2 +- components/product/Images.tsx | 2 +- components/product/PriceAndBuy.tsx | 2 +- components/product/ProductImage.tsx | 2 +- components/product/ProductMeta.tsx | 2 +- components/productsList/ProductImage.tsx | 2 +- components/productsList/ProductItem.tsx | 4 ++-- lib/{services => }/category.ts | 2 +- lib/{services => }/imgs.ts | 4 ++-- lib/{services => }/meta.ts | 0 lib/{services => }/price.ts | 2 +- lib/{services => }/urls.ts | 2 +- pages/category/[slug].tsx | 6 +++--- pages/product/[slug].tsx | 2 +- 19 files changed, 25 insertions(+), 25 deletions(-) rename lib/{services => }/category.ts (92%) rename lib/{services => }/imgs.ts (96%) rename lib/{services => }/meta.ts (100%) rename lib/{services => }/price.ts (95%) rename lib/{services => }/urls.ts (97%) diff --git a/components/BreadCrumbs.tsx b/components/BreadCrumbs.tsx index 3bebd32..2de2a81 100644 --- a/components/BreadCrumbs.tsx +++ b/components/BreadCrumbs.tsx @@ -2,7 +2,7 @@ import {ICategoryFlatItem} from 'boundless-api-client/types/catalog/category'; import clsx from 'clsx'; import Link from 'next/link'; import {TQuery} from '../@types/common'; -import {getCategoryUrl} from '../lib/services/urls'; +import {getCategoryUrl} from '../lib/urls'; export default function BreadCrumbs({parents, activeParams}: IBreadCrumbsProps) { const _parents = [...parents]; diff --git a/components/FilterForm.tsx b/components/FilterForm.tsx index c795344..9346be7 100644 --- a/components/FilterForm.tsx +++ b/components/FilterForm.tsx @@ -12,7 +12,7 @@ import TextCharacteristic from './filterForm/TextCharacteristic'; import BrandSelect from './filterForm/BrandSelect'; import Stock from './filterForm/Stock'; import _isEqualWith from 'lodash/isEqualWith'; -import {filterKeys, filterProductsQuery} from '../lib/services/category'; +import {filterKeys, filterProductsQuery} from '../lib/category'; /** * @param filterFields - might be passed manually, e.g. pass: diff --git a/components/cart/CartRow.tsx b/components/cart/CartRow.tsx index d6c5944..c6113b4 100644 --- a/components/cart/CartRow.tsx +++ b/components/cart/CartRow.tsx @@ -1,8 +1,8 @@ import {ICartItem} from 'boundless-api-client'; import Link from 'next/link'; import {formatMoney} from '../../lib/formatter'; -import {getCartImg} from '../../lib/services/imgs'; -import {getProductUrl} from '../../lib/services/urls'; +import {getCartImg} from '../../lib/imgs'; +import {getProductUrl} from '../../lib/urls'; export default function CartRow({item, rmItem, onQtyChange}: ICartRowProps) { return ( diff --git a/components/category/CategoryHomeMenu.tsx b/components/category/CategoryHomeMenu.tsx index 0e4b2f0..2471367 100644 --- a/components/category/CategoryHomeMenu.tsx +++ b/components/category/CategoryHomeMenu.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx'; import Link from 'next/link'; import {ICategoryPartial} from '../../@types/category'; -import {getCategoryImg} from '../../lib/services/imgs'; -import {getCategoryUrl} from '../../lib/services/urls'; +import {getCategoryImg} from '../../lib/imgs'; +import {getCategoryUrl} from '../../lib/urls'; export default function CategoryHomeMenu({categoryTree}: {categoryTree: ICategoryPartial[]}) { return ( diff --git a/components/category/Sidebar.tsx b/components/category/Sidebar.tsx index 5c92890..7cfee66 100644 --- a/components/category/Sidebar.tsx +++ b/components/category/Sidebar.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import Link from 'next/link'; -import {getCategoryImg} from '../../lib/services/imgs'; -import {getCategoryUrl} from '../../lib/services/urls'; +import {getCategoryImg} from '../../lib/imgs'; +import {getCategoryUrl} from '../../lib/urls'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faChevronLeft} from '@fortawesome/free-solid-svg-icons/faChevronLeft'; import {ICategoryFlatItem, ICategoryItem} from 'boundless-api-client/types/catalog/category'; diff --git a/components/header/ChooseVariantModal.tsx b/components/header/ChooseVariantModal.tsx index 3baf2df..1e18d27 100644 --- a/components/header/ChooseVariantModal.tsx +++ b/components/header/ChooseVariantModal.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import {useEffect, useState} from 'react'; import Modal from 'react-bootstrap/Modal'; import {useAppDispatch, useAppSelector} from '../../hooks/redux'; -import {getLowestPrice} from '../../lib/services/price'; +import {getLowestPrice} from '../../lib/price'; import {addItem2Cart} from '../../redux/actions/cart'; import {hideVariantModal} from '../../redux/reducers/cart'; import {RootState} from '../../redux/store'; diff --git a/components/product/Images.tsx b/components/product/Images.tsx index 8fe058a..553a974 100644 --- a/components/product/Images.tsx +++ b/components/product/Images.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react'; import {IProductImage} from 'boundless-api-client/types/image'; import clsx from 'clsx'; import ProductImage from './ProductImage'; -import {getMetaImgUrl, productImgRatio} from '../../lib/services/imgs'; +import {getMetaImgUrl, productImgRatio} from '../../lib/imgs'; import ImagesSlider from './ImagesSlider'; import NoImage from '../NoImage'; import {IProductItem} from 'boundless-api-client'; diff --git a/components/product/PriceAndBuy.tsx b/components/product/PriceAndBuy.tsx index 675d765..c76cc4e 100644 --- a/components/product/PriceAndBuy.tsx +++ b/components/product/PriceAndBuy.tsx @@ -1,7 +1,7 @@ import {useState} from 'react'; import {IProductItem, IProductVariant} from 'boundless-api-client'; import clsx from 'clsx'; -import {getLowestPrice, getProductPrice} from '../../lib/services/price'; +import {getLowestPrice, getProductPrice} from '../../lib/price'; import {useAppDispatch} from '../../hooks/redux'; import {addItem2Cart} from '../../redux/actions/cart'; import {getPriceForTpl} from '../../lib/product'; diff --git a/components/product/ProductImage.tsx b/components/product/ProductImage.tsx index 9d3258e..ed6e087 100644 --- a/components/product/ProductImage.tsx +++ b/components/product/ProductImage.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import {getProductImg, IImagePartial} from '../../lib/services/imgs'; +import {getProductImg, IImagePartial} from '../../lib/imgs'; export default function ProductImage({image, alt, maxSize = 800, preserveRatio = false}: IProductImageProps) { const {src, blurSrc, width, height} = getProductImg(image, maxSize, preserveRatio); diff --git a/components/product/ProductMeta.tsx b/components/product/ProductMeta.tsx index 6d8c18a..15172ff 100644 --- a/components/product/ProductMeta.tsx +++ b/components/product/ProductMeta.tsx @@ -1,5 +1,5 @@ import {ICategoryFlatItem, IProductItem} from 'boundless-api-client'; -import {getProductItemUrl} from '../../lib/services/urls'; +import {getProductItemUrl} from '../../lib/urls'; export default function ProductMeta({product, parents}: IProductMetaProps) { return ( diff --git a/components/productsList/ProductImage.tsx b/components/productsList/ProductImage.tsx index fd6408c..1f1a375 100644 --- a/components/productsList/ProductImage.tsx +++ b/components/productsList/ProductImage.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import {getProductsListImg, IImagePartial} from '../../lib/services/imgs'; +import {getProductsListImg, IImagePartial} from '../../lib/imgs'; export default function ProductListImage({image, alt, maxSize = 200}: {image: IImagePartial, alt: string, maxSize?: number}) { const {src, blurSrc, width, height} = getProductsListImg(image, maxSize); diff --git a/components/productsList/ProductItem.tsx b/components/productsList/ProductItem.tsx index 1a84676..4fac0b3 100644 --- a/components/productsList/ProductItem.tsx +++ b/components/productsList/ProductItem.tsx @@ -2,7 +2,7 @@ import {IProduct} from 'boundless-api-client/types/catalog/product'; import clsx from 'clsx'; import {useAppDispatch} from '../../hooks/redux'; import {addItem2Cart} from '../../redux/actions/cart'; -import {getProductUrl} from '../../lib/services/urls'; +import {getProductUrl} from '../../lib/urls'; import ProductListImage from './ProductImage'; import ProductPrice from './ProductPrice'; import {TQuery} from '../../@types/common'; @@ -11,7 +11,7 @@ import ProductLabels from '../product/Labels'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCartPlus} from '@fortawesome/free-solid-svg-icons/faCartPlus'; import NoImage from '../NoImage'; -import {productImgRatio} from '../../lib/services/imgs'; +import {productImgRatio} from '../../lib/imgs'; import {TThumbRatio} from '../../@types/image'; export default function ProductItem({product, query, categoryId}: IProductItemProps) { diff --git a/lib/services/category.ts b/lib/category.ts similarity index 92% rename from lib/services/category.ts rename to lib/category.ts index 9596ba4..7bc59fa 100644 --- a/lib/services/category.ts +++ b/lib/category.ts @@ -1,5 +1,5 @@ import {IGetProductsParams} from 'boundless-api-client/endpoints/catalog'; -import {TQuery} from '../../@types/common'; +import {TQuery} from '../@types/common'; export const filterKeys = ['brand', 'price_min', 'price_max', 'props', 'in_stock']; diff --git a/lib/services/imgs.ts b/lib/imgs.ts similarity index 96% rename from lib/services/imgs.ts rename to lib/imgs.ts index 8bf240c..920383e 100644 --- a/lib/services/imgs.ts +++ b/lib/imgs.ts @@ -1,5 +1,5 @@ -import {TThumbRatio} from '../../@types/image'; -import {apiClient} from '../api'; +import {TThumbRatio} from '../@types/image'; +import {apiClient} from './api'; //fixed aspect ratio for product images export const productImgRatio = process.env.BOUNDLESS_PRODUCTS_IMAGE_PROPORTION as TThumbRatio || null; diff --git a/lib/services/meta.ts b/lib/meta.ts similarity index 100% rename from lib/services/meta.ts rename to lib/meta.ts diff --git a/lib/services/price.ts b/lib/price.ts similarity index 95% rename from lib/services/price.ts rename to lib/price.ts index d63d8d2..f4593b8 100644 --- a/lib/services/price.ts +++ b/lib/price.ts @@ -1,5 +1,5 @@ import {IProductVariant} from 'boundless-api-client'; -import {calcBenefit} from '../calculator'; +import {calcBenefit} from './calculator'; import currency from 'currency.js'; export const getLowestPrice = (variants: IProductVariant[]): IPrice => { diff --git a/lib/services/urls.ts b/lib/urls.ts similarity index 97% rename from lib/services/urls.ts rename to lib/urls.ts index 6d04856..80829de 100644 --- a/lib/services/urls.ts +++ b/lib/urls.ts @@ -1,7 +1,7 @@ import {ICategory, ICategoryItem} from 'boundless-api-client'; import {IProduct, IProductItem} from 'boundless-api-client/types/catalog/product'; import {createGetStr} from 'boundless-api-client/utils'; -import {TQuery} from '../../@types/common'; +import {TQuery} from '../@types/common'; const CATEGORY_PREFIX = '/category'; const PRODUCTS_PREFIX = '/product'; diff --git a/pages/category/[slug].tsx b/pages/category/[slug].tsx index e4842d7..62ff847 100644 --- a/pages/category/[slug].tsx +++ b/pages/category/[slug].tsx @@ -11,14 +11,14 @@ import Pagination from '../../components/Pagination'; import {NextRouter, useRouter} from 'next/router'; import BreadCrumbs from '../../components/BreadCrumbs'; import CategorySidebar from '../../components/category/Sidebar'; -import {filterProductsQuery} from '../../lib/services/category'; +import {filterProductsQuery} from '../../lib/category'; import {TQuery} from '../../@types/common'; import FilterForm from '../../components/FilterForm'; import {createGetStr} from 'boundless-api-client/utils'; import qs from 'qs'; -import {getCategoryItemUrl} from '../../lib/services/urls'; +import {getCategoryItemUrl} from '../../lib/urls'; import SortButtons from '../../components/SortButtons'; -import {getCategoryMetaData} from '../../lib/services/meta'; +import {getCategoryMetaData} from '../../lib/meta'; export default function CategoryPage({errorCode, data}: InferGetServerSidePropsType) { const router = useRouter(); diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index ebf7331..b960ad9 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -11,7 +11,7 @@ import ProductPriceAndBuy from '../../components/product/PriceAndBuy'; import qs, {ParsedQs} from 'qs'; import ProductMeta from '../../components/product/ProductMeta'; import ProductCharacteristics from '../../components/product/ProductCharacteristics'; -import {getProductMetaData} from '../../lib/services/meta'; +import {getProductMetaData} from '../../lib/meta'; import ProductLabels from '../../components/product/Labels'; import ProductVariantAndBuy from '../../components/product/VariantAndBuy'; From e137b2269ce77a54a7289ed3219eb0c2a193a043 Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Wed, 19 Jan 2022 20:29:18 +0300 Subject: [PATCH 004/133] working --- ...haracteristics.tsx => Characteristics.tsx} | 10 +- .../{ProductMeta.tsx => MetaSchemaOrg.tsx} | 4 +- components/product/PriceAndBuy.tsx | 172 +++++++++++------- components/product/VariantAndBuy.tsx | 30 ++- components/product/VariantPicker.tsx | 18 +- .../CharacteristicItem.tsx} | 2 +- .../product/variantPicker/Characteristic.tsx | 97 +++++----- components/productsList/ProductPrice.tsx | 22 ++- lib/formatter.ts | 5 +- lib/price.ts | 8 - lib/product.ts | 26 +-- package.json | 4 +- pages/product/[slug].tsx | 20 +- styles/components/defaultVariables.scss | 4 + styles/components/product.scss | 2 +- styles/components/product/variantAndBuy.scss | 134 ++++++++++++++ styles/components/product/variantPicker.scss | 3 - yarn.lock | 20 +- 18 files changed, 364 insertions(+), 217 deletions(-) rename components/product/{ProductCharacteristics.tsx => Characteristics.tsx} (65%) rename components/product/{ProductMeta.tsx => MetaSchemaOrg.tsx} (94%) rename components/product/{CaracteristicItem.tsx => characteristics/CharacteristicItem.tsx} (86%) create mode 100644 styles/components/product/variantAndBuy.scss delete mode 100644 styles/components/product/variantPicker.scss diff --git a/components/product/ProductCharacteristics.tsx b/components/product/Characteristics.tsx similarity index 65% rename from components/product/ProductCharacteristics.tsx rename to components/product/Characteristics.tsx index f69509e..c7a0774 100644 --- a/components/product/ProductCharacteristics.tsx +++ b/components/product/Characteristics.tsx @@ -1,8 +1,8 @@ import {INonVariantCaracteristic} from 'boundless-api-client'; -import CaracteristicItem from './CaracteristicItem'; +import CharacteristicItem from './characteristics/CharacteristicItem'; export default function ProductCharacteristics({characteristics}: IProductCharacteristicsProps) { - if (!characteristics.length) return <>; + if (!characteristics.length) return null; return (
@@ -10,12 +10,12 @@ export default function ProductCharacteristics({characteristics}: IProductCharac
{characteristic.is_folder ? <> -
{characteristic.title}
+
{characteristic.title}
{characteristic.children?.map(child => ( - + ))} - : } + : }
))}
diff --git a/components/product/ProductMeta.tsx b/components/product/MetaSchemaOrg.tsx similarity index 94% rename from components/product/ProductMeta.tsx rename to components/product/MetaSchemaOrg.tsx index 15172ff..2dac3ad 100644 --- a/components/product/ProductMeta.tsx +++ b/components/product/MetaSchemaOrg.tsx @@ -1,7 +1,7 @@ import {ICategoryFlatItem, IProductItem} from 'boundless-api-client'; import {getProductItemUrl} from '../../lib/urls'; -export default function ProductMeta({product, parents}: IProductMetaProps) { +export default function MetaSchemaOrg({product, parents}: IProductMetaProps) { return ( <> @@ -12,7 +12,7 @@ export default function ProductMeta({product, parents}: IProductMetaProps) { {product.has_variants ? product.extendedVariants?.list.map(variant => (
- + {variant.price && } {variant.sku && } {variant.title && } diff --git a/components/product/PriceAndBuy.tsx b/components/product/PriceAndBuy.tsx index c76cc4e..4a99207 100644 --- a/components/product/PriceAndBuy.tsx +++ b/components/product/PriceAndBuy.tsx @@ -1,81 +1,119 @@ -import {useState} from 'react'; +import {useState, useMemo, ChangeEvent, MouseEvent} from 'react'; import {IProductItem, IProductVariant} from 'boundless-api-client'; import clsx from 'clsx'; -import {getLowestPrice, getProductPrice} from '../../lib/price'; import {useAppDispatch} from '../../hooks/redux'; import {addItem2Cart} from '../../redux/actions/cart'; -import {getPriceForTpl} from '../../lib/product'; +import {getPriceForTpl, IPriceForTpl} from '../../lib/product'; +import {formatMoney} from '../../lib/formatter'; +import currency from 'currency.js'; +import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'; +import {faMinus} from '@fortawesome/free-solid-svg-icons/faMinus'; +import {faCartPlus} from '@fortawesome/free-solid-svg-icons/faCartPlus'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -export default function ProductPriceAndBuy({product, selectedVariant}: IPriceAndBuyProps) { - // const dispatch = useAppDispatch(); - // const hasVariants = product.has_variants || false; - // const price = hasVariants - // ? getLowestPrice(selectedVariants) - // : getProductPrice(product.price, product.old_price); - // - // const variantPicked = selectedVariants.length === 1; - // const isVariantAvailable = selectedVariants.length > 0; - // const [variant] = selectedVariants; - // const inStock = hasVariants ? variant?.in_stock || false : product.in_stock || false; - // const [qty, setQty] = useState(1); - // - // const onSubmit = () => { - // if ((hasVariants && !variantPicked) || !inStock) { - // setError(true); - // } else { - // dispatch(addItem2Cart(hasVariants ? variant.item_id : product.item_id, qty)); - // } - // }; +export default function ProductPriceAndBuy({product, selectedVariant, setError}: IPriceAndBuyProps) { + const dispatch = useAppDispatch(); + const [qty, setQty] = useState(1); - const price = getPriceForTpl(product.price, product.old_price); + const {price, benefit, isInStock} = useMemo(() => { + let price: IPriceForTpl, benefit: number|null = null; + if (selectedVariant) { + price = {price: selectedVariant.price, oldPrice: selectedVariant.price_old}; + } else { + price = getPriceForTpl(product.price); + } + + if (price.price && price.oldPrice) { + benefit = new currency(price.oldPrice).subtract(price.price).toJSON(); + } + + const isInStock = selectedVariant ? selectedVariant.in_stock : product.in_stock; + + return {price, benefit, isInStock}; + }, [product, selectedVariant]); + + const onBuyBtnClicked = (e: MouseEvent) => { + e.preventDefault(); + + if (product.has_variants && !selectedVariant) { + setError('Please, choose a variant.'); + return; + } + + dispatch(addItem2Cart(selectedVariant ? selectedVariant.item_id : product.item_id, qty)); + }; return (
- {/*

*/} - {/* {!variantPicked && hasVariants && From: }*/} - {/* {price.price}*/} - {/* {price.price_old && {price.price_old}}*/} - {/*

*/} - {/*{price.benefit &&

*/} - {/* */} - {/* {price.benefit}*/} - {/*

}*/} - {/*{(variantPicked || !hasVariants) &&

*/} - {/* {inStock ? 'In stock' : 'Out of stock'}*/} - {/*

}*/} - {/*{inStock &&*/} - {/*
*/} - {/*
*/} - {/* setQty(qty - 1)}*/} - {/* ><>–*/} - {/* setQty(Number(e.target.value) || 0)}*/} - {/* />*/} - {/* setQty(qty + 1)}*/} - {/* >+*/} - {/*
*/} - {/* */} - {/*
}*/} + {price.price &&

+ {price.isFrom && From:} + {formatMoney(price.price)} + {price.oldPrice && {formatMoney(price.oldPrice)}} +

} + {benefit &&

+ + {formatMoney(benefit)} +

} + {(!product.has_variants || selectedVariant) &&

+ {isInStock && 'In stock'} + {!isInStock && 'Out of stock'} +

} + {isInStock !== false &&
+ +
+ +
+
}
); } interface IPriceAndBuyProps { - // selectedVariants: IProductVariant[] | []; - // setError: (value: boolean) => void; - product: IProductItem; - selectedVariant?: IProductVariant -} \ No newline at end of file + product: Pick; + selectedVariant?: IProductVariant|null; + setError: (error: null|string) => void; +} + +const PriceAndBuyQty = ({qty, setQty}: {qty: number, setQty: (value: number) => void}) => { + const onChange = (e: ChangeEvent) => setQty(parseInt(e.target.value) || 1); + const onBtnClicked = (diff: number, e: MouseEvent) => { + e.preventDefault(); + + let newQty = qty + diff; + if (newQty < 1) { + newQty = 1; + } + + setQty(newQty); + }; + + return ( +
+ + + +
+ ); +}; \ No newline at end of file diff --git a/components/product/VariantAndBuy.tsx b/components/product/VariantAndBuy.tsx index 721f46a..b841f04 100644 --- a/components/product/VariantAndBuy.tsx +++ b/components/product/VariantAndBuy.tsx @@ -1,17 +1,29 @@ -import {IProductItem} from 'boundless-api-client'; +import {IProductItem, IProductVariant} from 'boundless-api-client'; import ProductVariantPicker from './VariantPicker'; import ProductPriceAndBuy from './PriceAndBuy'; - +import {useState} from 'react'; +import clsx from 'clsx'; export default function ProductVariantAndBuy({product}: {product: IProductItem}) { - const onCaseChange = () => {}; + const [selectedVariant, setSelectedVariant] = useState(); + const [error, setError] = useState(); + const onCaseChange = (value: {}, variant?: IProductVariant) => { + setSelectedVariant(variant ? variant : null); + setError(null); + }; return ( - <> - {product.has_variants && } - - +
+ {product.has_variants &&
+ + {error &&
{error}
} +
} + +
); } \ No newline at end of file diff --git a/components/product/VariantPicker.tsx b/components/product/VariantPicker.tsx index 16418b9..cc6352b 100644 --- a/components/product/VariantPicker.tsx +++ b/components/product/VariantPicker.tsx @@ -5,10 +5,11 @@ import {IVariantCombination} from 'boundless-api-client/src/types/catalog/varian import _isEqual from 'lodash/isEqual'; export default function ProductVariantPicker({extendedVariants, onChange}: IVariantPickerProps) { - const {characteristics, list, combinations} = extendedVariants; + const {characteristics, list, combinations, idCombinations} = extendedVariants; const [value, setValue] = useState<{[characteristicId: number]: number}>({}); const onSelectCase = (characteristicId: number, caseId: number|null) => { + console.log('characteristicId:', characteristicId, caseId); const newValue = {...value}; if (caseId === null) { delete newValue[characteristicId]; @@ -36,17 +37,10 @@ export default function ProductVariantPicker({extendedVariants, onChange}: IVari key={characteristic.id} onSelectCase={onSelectCase} value={value} - // values={values} - // idCombinations={idCombinations} - // onSelect={variantSelected} + idCombinations={idCombinations} + variants={list} /> - // d-flex gap-2 mb-3 align-items-center - //
- //
))} - {/*{error &&
*/} - {/* Please pick a product variant*/} - {/*
}*/}
); } @@ -59,9 +53,7 @@ interface IVariantPickerProps { const findVariantIdByCombinations = (value: {[key: number]: number}, combinations: IVariantCombination): null|string => { const requiredCombinations = Object.entries(value).map(([characteristicId, caseId]) => `${characteristicId}-${caseId}`); - const result = Object.entries(combinations).find(([variantId, variantCombination]) => { - return _isEqual(requiredCombinations, variantCombination); - }); + const result = Object.entries(combinations).find(([variantId, variantCombination]) => _isEqual(requiredCombinations, variantCombination)); return result ? result[0] : null; }; \ No newline at end of file diff --git a/components/product/CaracteristicItem.tsx b/components/product/characteristics/CharacteristicItem.tsx similarity index 86% rename from components/product/CaracteristicItem.tsx rename to components/product/characteristics/CharacteristicItem.tsx index fb08e34..547dd59 100644 --- a/components/product/CaracteristicItem.tsx +++ b/components/product/characteristics/CharacteristicItem.tsx @@ -1,6 +1,6 @@ import {INonVariantCaracteristic} from 'boundless-api-client'; -export default function CaracteristicItem({characteristic}: {characteristic: INonVariantCaracteristic}) { +export default function CharacteristicItem({characteristic}: {characteristic: INonVariantCaracteristic}) { return ( <>
diff --git a/components/product/variantPicker/Characteristic.tsx b/components/product/variantPicker/Characteristic.tsx index 965942a..4b827ea 100644 --- a/components/product/variantPicker/Characteristic.tsx +++ b/components/product/variantPicker/Characteristic.tsx @@ -1,52 +1,13 @@ import {IVariantCharacteristic, IVariantIdCombinations} from 'boundless-api-client'; -import {ChangeEvent, useEffect, useRef} from 'react'; +import {ChangeEvent, useEffect, useMemo, useRef, MouseEvent} from 'react'; +import {IProductVariant} from 'boundless-api-client/src/types/catalog/variant'; +import clsx from 'clsx'; // values, onSelect, idCombinations -export default function VariantPickerCharacteristic({characteristic, onSelectCase, value}: IVariantCharProps) { - /* - const value = values[characteristic.id]; - const submitted = useRef(false); - - const checkAvailable = (caseId: number) => { - const _values = {...values}; - delete _values[characteristic.id]; - if (!Object.keys(_values).length) return true; - - for (const combination of Object.values(idCombinations)) { - let out = true; - for (const [key, value] of Object.entries(_values)) { - out = out && combination[key] === value; - } - if (out && combination[characteristic.id] === caseId) return true; - } - - return false; - }; - - const checkRowAvailability = () => { - const available = characteristic.cases.filter(caseItem => checkAvailable(caseItem.id)); - - if (available.length === 1) { - onSubmit(available[0].id); - } - }; - - const onSubmit = (id: number) => { - submitted.current = true; - onSelect(characteristic.id, id); - }; - - useEffect(() => { - if (!submitted.current) { - checkRowAvailability(); - } else { - submitted.current = false; - } - }, [values, characteristic]); //eslint-disable-line -*/ - - const onInputChange = (caseId: number, e: ChangeEvent) => { - onSelectCase(characteristic.id, e.currentTarget.checked ? caseId : null); +export default function VariantPickerCharacteristic({characteristic, onSelectCase, value, idCombinations, variants}: IVariantCharProps) { + const onLabelClicked = (caseId: number, e: MouseEvent) => { + e.preventDefault(); + onSelectCase(characteristic.id, value[characteristic.id] === caseId ? null : caseId); }; return ( @@ -55,20 +16,27 @@ export default function VariantPickerCharacteristic({characteristic, onSelectCas
{characteristic.cases.map(caseItem => { const id = `${characteristic.id}-case-${caseItem.id}`; + const availableVariants = findAvailableVariants(variants, idCombinations, {...value, ...{[characteristic.id]: caseItem.id}}); + const inStockVariants = availableVariants.filter(({in_stock}) => in_stock); return ( -
+
-
@@ -81,7 +49,30 @@ export default function VariantPickerCharacteristic({characteristic, onSelectCas interface IVariantCharProps { characteristic: IVariantCharacteristic; - value: {[key: number]: number}; + value: {[characteristicId: number|string]: number}; onSelectCase: (characteristicId: number, caseId: number|null) => void; - // idCombinations: IVariantIdCombinations; -} \ No newline at end of file + idCombinations: IVariantIdCombinations; + variants: IProductVariant[] +} + +const findAvailableVariants = (variants: IProductVariant[], idCombinations: IVariantIdCombinations, value: {[characteristicId: number|string]: number}): IProductVariant[] => { + const variantIds: number[] = []; + + for (const [variantId, combination] of Object.entries(idCombinations)) { + if (isValueSuitsCombination(combination, value)) { + variantIds.push(parseInt(variantId)); + } + } + + return variants.filter(({variant_id}) => variantIds.includes(variant_id)); +}; + +const isValueSuitsCombination = (combination: {[characteristicId: number|string]: number}, value: {[characteristicId: number|string]: number}): boolean => { + for (const [characteristicId, caseId] of Object.entries(value)) { + if (!(characteristicId in combination) || combination[characteristicId] != caseId) { + return false; + } + } + + return true; +}; \ No newline at end of file diff --git a/components/productsList/ProductPrice.tsx b/components/productsList/ProductPrice.tsx index af345ef..972b901 100644 --- a/components/productsList/ProductPrice.tsx +++ b/components/productsList/ProductPrice.tsx @@ -1,19 +1,21 @@ import {IProductPrice} from 'boundless-api-client/types/catalog/product'; import clsx from 'clsx'; import {formatMoney} from '../../lib/formatter'; +import {getPriceForTpl} from '../../lib/product'; -export default function ProductPrice({price, className = 'products__price'}: {price: IPricePartial, className?: string}) { - const {old, old_max, old_min, max, min, value} = price; - if (!max && !min && !value) return <>; - const hasOld = !!old || !!old_min || !!old_max; +export default function ProductPrice({price, className = 'products__price'}: {price: IProductPrice, className?: string}) { + const tplPrice = getPriceForTpl(price); + + if (tplPrice.price === null) + return null; return (
- {!!min && From:} - {hasOld && {formatMoney(old_min || old)}} - {formatMoney(min || value)} + {tplPrice.isFrom && From:} + {tplPrice.oldPrice && {formatMoney(tplPrice.oldPrice)}} + + {formatMoney(tplPrice.price)} +
); -} - -type IPricePartial = Pick \ No newline at end of file +} \ No newline at end of file diff --git a/lib/formatter.ts b/lib/formatter.ts index 6ea8153..570e07c 100644 --- a/lib/formatter.ts +++ b/lib/formatter.ts @@ -1,6 +1,7 @@ -import accounting from 'accounting'; +import currency from 'currency.js'; export function formatMoney(amount: number|string|null): string { if (!amount) return ''; - return accounting.formatMoney(amount); + + return new currency(amount).format(); } \ No newline at end of file diff --git a/lib/price.ts b/lib/price.ts index f4593b8..413cb9a 100644 --- a/lib/price.ts +++ b/lib/price.ts @@ -22,14 +22,6 @@ export const getLowestPrice = (variants: IProductVariant[]): IPrice => { }; }; -export const getProductPrice = (price: number, price_old: number|null): IPrice => { - return { - price: currency(price).format(), - price_old: price_old && currency(price_old).format() , - benefit: calcBenefit(price, price_old) - }; -}; - export interface IPrice { price: string|number; price_old: string|number|null; diff --git a/lib/product.ts b/lib/product.ts index a1ec12d..05e5e39 100644 --- a/lib/product.ts +++ b/lib/product.ts @@ -1,19 +1,19 @@ +import {IProductPrice} from 'boundless-api-client/types/catalog/product'; + export interface IPriceForTpl { - price: number, - oldPrice: number|null, - isFrom: boolean + price: number|null, + oldPrice?: number|null, + isFrom?: boolean } -export const getPriceForTpl = (curPrice: number|number[], oldPrice: null|number|number[]): IPriceForTpl => { - const out: IPriceForTpl = { - price: Array.isArray(curPrice) ? curPrice[0] : curPrice, - oldPrice: null, - isFrom: (Array.isArray(curPrice) && curPrice[0] != curPrice[1]) ? true : false - }; - - if (oldPrice && !(Array.isArray(curPrice) && !Array.isArray(oldPrice))) { - out.oldPrice = Array.isArray(oldPrice) ? oldPrice[0] : oldPrice; +export const getPriceForTpl = (price: IProductPrice|null): IPriceForTpl => { + if (!price) { + return {price: null}; } - return out; + return { + price: price.min ? price.min : price.value, + oldPrice: price.old_min ? price.old_min : price.old, + isFrom: (price.min && price.max && price.min != price.max) ? true : false + }; }; \ No newline at end of file diff --git a/package.json b/package.json index 5b8c737..6e0b237 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,14 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "@reduxjs/toolkit": "^1.7.1", - "@types/accounting": "^0.4.2", "@types/js-cookie": "^3.0.1", "@types/lodash": "^4.14.178", "@types/nprogress": "^0.2.0", "@types/react-redux": "^7.1.21", - "accounting": "^0.4.1", "bootstrap": "^5.1.3", "boundless-api-client": "ssh://git@github.com:kirill-zhirnov/boundless-api-client", "clsx": "^1.1.1", - "currency.js": "^2.0.4", + "currency.js": "^2.0.4-68e210f8", "js-cookie": "^3.0.1", "lodash": "^4.17.21", "next": "^12.0.7", diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index b960ad9..65e1a8c 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -1,25 +1,21 @@ import {useEffect, useMemo, useState} from 'react'; -import {ICategoryFlatItem, IProductItem, IProductVariant} from 'boundless-api-client'; +import {ICategoryFlatItem, IProductItem} from 'boundless-api-client'; import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next'; import MainLayout from '../../layouts/Main'; import {apiClient} from '../../lib/api'; import {useRouter} from 'next/router'; import BreadCrumbs from '../../components/BreadCrumbs'; import ProductImages from '../../components/product/Images'; -import ProductVariantPicker from '../../components/product/VariantPicker'; -import ProductPriceAndBuy from '../../components/product/PriceAndBuy'; import qs, {ParsedQs} from 'qs'; -import ProductMeta from '../../components/product/ProductMeta'; -import ProductCharacteristics from '../../components/product/ProductCharacteristics'; +import MetaSchemaOrg from '../../components/product/MetaSchemaOrg'; import {getProductMetaData} from '../../lib/meta'; import ProductLabels from '../../components/product/Labels'; import ProductVariantAndBuy from '../../components/product/VariantAndBuy'; +import ProductCharacteristics from '../../components/product/Characteristics'; export default function ProductPage({data}: InferGetStaticPropsType) { const {product, categoryParents} = data; const [resolvedParents, setResolvedParents] = useState(categoryParents); - const [selectedVariants, setSelectedVariants] = useState(product.extendedVariants?.list || []); - const [error, setError] = useState(false); const router = useRouter(); const query = useMemo(() => qs.parse(router.asPath.split('?')[1] || ''), [router.asPath]); @@ -53,16 +49,16 @@ export default function ProductPage({data}: InferGetStaticPropsType
- {/**/} - {/**/} - {/*

Shipping

*/} - {/*We ship ASAP!*/} +
+ +

Shipping

+ We ship ASAP!
{product.text.description &&
} - +
diff --git a/styles/components/defaultVariables.scss b/styles/components/defaultVariables.scss index 0c00b7f..835f7b5 100644 --- a/styles/components/defaultVariables.scss +++ b/styles/components/defaultVariables.scss @@ -22,3 +22,7 @@ $front-category-sidebar-color: $front-text-color; $front-category-sidebar-hover-color: $front-highlight-link-color; $front-products-border-color: #cfd3da; + +$front-error-color: #a94442; + +$front-hr-color: #eee; diff --git a/styles/components/product.scss b/styles/components/product.scss index bed6494..34a2d98 100644 --- a/styles/components/product.scss +++ b/styles/components/product.scss @@ -1,6 +1,6 @@ @import './product/labels'; @import './product/gallery'; -@import './product/variantPicker'; +@import './product/variantAndBuy'; .product-description { max-width: 800px; diff --git a/styles/components/product/variantAndBuy.scss b/styles/components/product/variantAndBuy.scss new file mode 100644 index 0000000..c1f23ac --- /dev/null +++ b/styles/components/product/variantAndBuy.scss @@ -0,0 +1,134 @@ +.variant-picker { + margin: 0 0 1rem; + + &__characteristic { + align-items: center; + display: flex; + flex-wrap: wrap; + margin: 0 0 0.75rem; + + @include media-breakpoint-up(sm) { + flex-wrap: nowrap; + } + + &:nth-child(even) { + background: $front-bg-color; + } + } + + &__title { + margin: 5px 25px 5px 5px; + white-space: nowrap; + } + + &__cases { + display: flex; + flex-wrap: wrap; + } + + &__case-item { + margin: 5px; + + .btn { + &.out-of-stock { + cursor: not-allowed; + } + } + } +} + +.variant-and-buy { + &__error { + color: $front-error-color; + margin: 0 0 1rem; + } + + &__variants { + border-bottom: 1px solid $front-hr-color; + margin: 0 0 1rem; + + &.has-error { + .variant-picker { + background: #f2dede; + border: 1px solid #ebccd1; + padding: 4px; + border-radius: 4px; + } + } + } +} + +.price-and-buy { + &__price { + margin: 0 -5px 1rem; + } + + &__from { + color: $text-muted; + display: inline-block; + font-size: 0.9rem; + margin: 0 5px; + } + + &__old { + display: inline-block; + color: $text-muted; + margin: 0 5px; + text-decoration: line-through; + font-size: 1.5rem; + } + + &__current { + display: inline-block; + margin: 0 5px; + font-size: 1.8rem; + + &.has-old { + color: $front-highlighted-price-color; + } + } + + &__benefit-label { + margin: 0 5px 0 0; + } + + &__benefit-value { + color: $front-highlighted-price-color; + } + + &__2-cart { + margin: -5px -10px 1rem -10px; + + display: flex; + align-items: flex-start; + flex-wrap: wrap; + } + + &__btns { + .btn { + margin: 5px 10px; + } + } + + &__qty { + width: 130px; + flex-shrink: 0; + margin: 5px 10px; + + .form-control { + text-align: center; + } + } + + &__stock { + &.in { + color: $green; + } + + &.out { + font-size: 1.5rem; + font-weight: $front-font-weight-bold; + color: $text-muted; + } + } +} \ No newline at end of file diff --git a/styles/components/product/variantPicker.scss b/styles/components/product/variantPicker.scss deleted file mode 100644 index 06d9507..0000000 --- a/styles/components/product/variantPicker.scss +++ /dev/null @@ -1,3 +0,0 @@ -.variant-picker { - -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2b7f53e..e1885f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,11 +293,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@types/accounting@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@types/accounting/-/accounting-0.4.2.tgz#81239fc56bc62511a3d3ff535dde2e0bbf3c53c1" - integrity sha512-M8W6tsLZguGdRaSQMmg58PUM/5HgoDuBGVyMiYNGi0FZkLzJDjUBGGYRYj1J9OWELCTqfux+ogVyKObCzfWJgg== - "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -482,11 +477,6 @@ "@typescript-eslint/types" "5.9.1" eslint-visitor-keys "^3.0.0" -accounting@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/accounting/-/accounting-0.4.1.tgz#87dd4103eff7f4460f1e186f5c677ed6cf566883" - integrity sha1-h91BA+/39EYPHhhvXGd+1s9WaIM= - acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -665,7 +655,7 @@ bootstrap@^5.1.3: "boundless-api-client@ssh://git@github.com:kirill-zhirnov/boundless-api-client": version "0.0.1" - resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#36b455cbd0c6fda492f882881b2948f69406e61b" + resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#b3448a520fe3af51674ec93633aa39c35c1fada3" dependencies: axios "^0.24.0" jsonwebtoken "^8.5.1" @@ -857,10 +847,10 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== -currency.js@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-2.0.4.tgz#a8a4d69be3b2e509bf67a560c78220bc04809cf1" - integrity sha512-6/OplJYgJ0RUlli74d93HJ/OsKVBi8lB1+Z6eJYS1YZzBuIp4qKKHpJ7ad+GvTlWmLR/hLJOWTykN5Nm8NJ7+w== +currency.js@^2.0.4-68e210f8: + version "2.0.4-68e210f8" + resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-2.0.4-68e210f8.tgz#a7ebc543cb77796c3428cff4f32dc9f164acf835" + integrity sha512-ZBeFgod0Un+KPUnn4O3ie15Ewdbz58bF4Ptw5lPNQWWtbLBRNhNmdA5lRmkDs7i9t+cR+GFLhu2Of5m69hDwVw== damerau-levenshtein@^1.0.7: version "1.0.8" From 9bbe740c78b031cc4bca3d850f0f31aa1cc54b05 Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Thu, 20 Jan 2022 18:59:00 +0300 Subject: [PATCH 005/133] working --- components/NoImage.tsx | 5 +- .../{CategoryHomeMenu.tsx => HomeMenu.tsx} | 0 components/header/CallToOrder.tsx | 10 ++- components/header/ChooseVariantModal.tsx | 74 ++++++------------- components/product/Images.tsx | 1 - components/product/PriceAndBuy.tsx | 10 ++- components/product/VariantAndBuy.tsx | 9 ++- components/product/VariantPicker.tsx | 2 +- .../product/variantPicker/Characteristic.tsx | 4 +- components/productsList/ProductImage.tsx | 2 +- components/productsList/ProductItem.tsx | 2 +- lib/calculator.ts | 8 +- lib/price.ts | 29 -------- pages/category/[slug].tsx | 9 ++- pages/index.tsx | 2 +- redux/actions/cart.ts | 6 +- redux/reducers/cart.ts | 3 +- styles/components/noImage.scss | 6 ++ styles/components/product/variantAndBuy.scss | 18 ++--- yarn.lock | 2 +- 20 files changed, 81 insertions(+), 121 deletions(-) rename components/category/{CategoryHomeMenu.tsx => HomeMenu.tsx} (100%) delete mode 100644 lib/price.ts diff --git a/components/NoImage.tsx b/components/NoImage.tsx index f738970..38c54ca 100644 --- a/components/NoImage.tsx +++ b/components/NoImage.tsx @@ -1,8 +1,9 @@ import {TThumbRatio} from '../@types/image'; +import clsx from 'clsx'; -export default function NoImage({ratio}: {ratio: TThumbRatio}) { +export default function NoImage({ratio, className}: {ratio: TThumbRatio, className?: string}) { return ( -
+
); diff --git a/components/category/CategoryHomeMenu.tsx b/components/category/HomeMenu.tsx similarity index 100% rename from components/category/CategoryHomeMenu.tsx rename to components/category/HomeMenu.tsx diff --git a/components/header/CallToOrder.tsx b/components/header/CallToOrder.tsx index 9a00d60..57a08c7 100644 --- a/components/header/CallToOrder.tsx +++ b/components/header/CallToOrder.tsx @@ -8,7 +8,9 @@ import {RootState} from '../../redux/store'; import ProductImage from '../productsList/ProductImage'; import NoImage from '../NoImage'; import {TThumbRatio} from '../../@types/image'; -import ProductPrice from '../productsList/ProductPrice'; +import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {formatMoney} from '../../lib/formatter'; export default function CallToOrder() { const dispatch = useAppDispatch(); @@ -52,13 +54,15 @@ export default function CallToOrder() {
{item.prices.length > 0 &&
- {` x ${qty} = ${item.prices[0].value ? calcTotalPrice(item.prices[0].value, qty!) : ''}`} + {`${formatMoney(item.prices[0].value)} x ${qty} = ${calcTotalPrice(item.prices[0].value!, qty!)}`}
} }
diff --git a/components/header/ChooseVariantModal.tsx b/components/header/ChooseVariantModal.tsx index 1e18d27..a2340cf 100644 --- a/components/header/ChooseVariantModal.tsx +++ b/components/header/ChooseVariantModal.tsx @@ -1,71 +1,41 @@ -import clsx from 'clsx'; -import {useEffect, useState} from 'react'; import Modal from 'react-bootstrap/Modal'; import {useAppDispatch, useAppSelector} from '../../hooks/redux'; -import {getLowestPrice} from '../../lib/price'; -import {addItem2Cart} from '../../redux/actions/cart'; import {hideVariantModal} from '../../redux/reducers/cart'; import {RootState} from '../../redux/store'; -import ProductVariantPicker from '../product/VariantPicker'; +import ProductVariantAndBuy from '../product/VariantAndBuy'; +import ProductImage from '../productsList/ProductImage'; +import NoImage from '../NoImage'; +import {TThumbRatio} from '../../@types/image'; export default function ChooseVariantModal() { const dispatch = useAppDispatch(); const show = useAppSelector((state: RootState) => state.cart.showVariantModal); - const {product, variants} = useAppSelector((state: RootState) => state.cart.variantModalData); - const [error, setError] = useState(false); - const [filteredVariants, setFilteredVariants] = useState(variants?.list || []); - const [variant] = filteredVariants; - const price = getLowestPrice(filteredVariants); - const inStock = variant?.in_stock || false; + const {product} = useAppSelector((state: RootState) => state.cart.variantModalData); - const variantPicked = filteredVariants.length === 1; - - const onHide = () => { - dispatch(hideVariantModal()); - }; - - useEffect(() => { - if (show) { - setFilteredVariants(variants?.list || []); - } - }, [show, variants]); - - const onSubmit = () => { - if (!variant || !variantPicked) return; - - dispatch(addItem2Cart(variant.item_id)); - onHide(); - }; - - if (!variants) return <>; + const onHide = () => dispatch(hideVariantModal()); + const image = product ? product.images.find(({is_default}) => is_default) : undefined; return ( - Add to cart "{product?.text.title}" + Please, choose a variant: -
Please select a variant:
- - {variantPicked &&
-

- {!variantPicked && From: } - {price.price} - {price.price_old && {price.price_old}} -

-

- {inStock ? 'In stock' : 'Out of stock'} -

-
} + {product && <> +
+
+ {image + ? + : + } +
+
{product.text.title}
+
+ + }
- - - -
); } \ No newline at end of file diff --git a/components/product/Images.tsx b/components/product/Images.tsx index 553a974..8cbf61b 100644 --- a/components/product/Images.tsx +++ b/components/product/Images.tsx @@ -1,5 +1,4 @@ import React, {useState} from 'react'; -import {IProductImage} from 'boundless-api-client/types/image'; import clsx from 'clsx'; import ProductImage from './ProductImage'; import {getMetaImgUrl, productImgRatio} from '../../lib/imgs'; diff --git a/components/product/PriceAndBuy.tsx b/components/product/PriceAndBuy.tsx index 4a99207..25c96ca 100644 --- a/components/product/PriceAndBuy.tsx +++ b/components/product/PriceAndBuy.tsx @@ -11,7 +11,7 @@ import {faMinus} from '@fortawesome/free-solid-svg-icons/faMinus'; import {faCartPlus} from '@fortawesome/free-solid-svg-icons/faCartPlus'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -export default function ProductPriceAndBuy({product, selectedVariant, setError}: IPriceAndBuyProps) { +export default function ProductPriceAndBuy({product, selectedVariant, setError, onAddedToCart}: IPriceAndBuyProps) { const dispatch = useAppDispatch(); const [qty, setQty] = useState(1); @@ -40,7 +40,12 @@ export default function ProductPriceAndBuy({product, selectedVariant, setError}: return; } - dispatch(addItem2Cart(selectedVariant ? selectedVariant.item_id : product.item_id, qty)); + const itemId = selectedVariant ? selectedVariant.item_id : product.item_id; + dispatch(addItem2Cart(itemId, qty)); + + if (onAddedToCart) { + onAddedToCart(itemId, qty); + } }; return ( @@ -79,6 +84,7 @@ interface IPriceAndBuyProps { product: Pick; selectedVariant?: IProductVariant|null; setError: (error: null|string) => void; + onAddedToCart?: (itemId: number, qty: number) => void; } const PriceAndBuyQty = ({qty, setQty}: {qty: number, setQty: (value: number) => void}) => { diff --git a/components/product/VariantAndBuy.tsx b/components/product/VariantAndBuy.tsx index b841f04..6b54f01 100644 --- a/components/product/VariantAndBuy.tsx +++ b/components/product/VariantAndBuy.tsx @@ -4,9 +4,10 @@ import ProductPriceAndBuy from './PriceAndBuy'; import {useState} from 'react'; import clsx from 'clsx'; -export default function ProductVariantAndBuy({product}: {product: IProductItem}) { +export default function ProductVariantAndBuy({product, onAddedToCart}: IProductVariantAndBuyProps) { const [selectedVariant, setSelectedVariant] = useState(); const [error, setError] = useState(); + const onCaseChange = (value: {}, variant?: IProductVariant) => { setSelectedVariant(variant ? variant : null); setError(null); @@ -23,7 +24,13 @@ export default function ProductVariantAndBuy({product}: {product: IProductItem}) ); +} + +interface IProductVariantAndBuyProps { + product: Pick, + onAddedToCart?: (itemId: number, qty: number) => void; } \ No newline at end of file diff --git a/components/product/VariantPicker.tsx b/components/product/VariantPicker.tsx index cc6352b..b34da84 100644 --- a/components/product/VariantPicker.tsx +++ b/components/product/VariantPicker.tsx @@ -9,7 +9,6 @@ export default function ProductVariantPicker({extendedVariants, onChange}: IVari const [value, setValue] = useState<{[characteristicId: number]: number}>({}); const onSelectCase = (characteristicId: number, caseId: number|null) => { - console.log('characteristicId:', characteristicId, caseId); const newValue = {...value}; if (caseId === null) { delete newValue[characteristicId]; @@ -53,6 +52,7 @@ interface IVariantPickerProps { const findVariantIdByCombinations = (value: {[key: number]: number}, combinations: IVariantCombination): null|string => { const requiredCombinations = Object.entries(value).map(([characteristicId, caseId]) => `${characteristicId}-${caseId}`); + //eslint-disable-next-line const result = Object.entries(combinations).find(([variantId, variantCombination]) => _isEqual(requiredCombinations, variantCombination)); return result ? result[0] : null; diff --git a/components/product/variantPicker/Characteristic.tsx b/components/product/variantPicker/Characteristic.tsx index 4b827ea..21fb9ed 100644 --- a/components/product/variantPicker/Characteristic.tsx +++ b/components/product/variantPicker/Characteristic.tsx @@ -1,5 +1,5 @@ import {IVariantCharacteristic, IVariantIdCombinations} from 'boundless-api-client'; -import {ChangeEvent, useEffect, useMemo, useRef, MouseEvent} from 'react'; +import {MouseEvent} from 'react'; import {IProductVariant} from 'boundless-api-client/src/types/catalog/variant'; import clsx from 'clsx'; @@ -27,7 +27,7 @@ export default function VariantPickerCharacteristic({characteristic, onSelectCas className={'btn-check'} disabled={!availableVariants.length} name={`characteristic-${characteristic.id}`} - // onChange={onInputChange.bind(null, caseItem.id)} + onChange={() => {}} type={'radio'} checked={value[characteristic.id] === caseItem.id} value={caseItem.id} diff --git a/components/productsList/ProductImage.tsx b/components/productsList/ProductImage.tsx index 1f1a375..ffbf26a 100644 --- a/components/productsList/ProductImage.tsx +++ b/components/productsList/ProductImage.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import {getProductsListImg, IImagePartial} from '../../lib/imgs'; -export default function ProductListImage({image, alt, maxSize = 200}: {image: IImagePartial, alt: string, maxSize?: number}) { +export default function ProductListImage({image, alt, maxSize = 200}: {image: IImagePartial, alt?: string, maxSize?: number}) { const {src, blurSrc, width, height} = getProductsListImg(image, maxSize); return ( diff --git a/components/productsList/ProductItem.tsx b/components/productsList/ProductItem.tsx index 4fac0b3..2b32c7b 100644 --- a/components/productsList/ProductItem.tsx +++ b/components/productsList/ProductItem.tsx @@ -50,7 +50,7 @@ export default function ProductItem({product, query, categoryId}: IProductItemPr function Product2Cart({product}: {product: IProduct}) { const dispatch = useAppDispatch(); - const onAddToCart = () => dispatch(addItem2Cart(product.item_id!, 1)); + const onAddToCart = () => dispatch(addItem2Cart(product.item_id, 1)); return (
diff --git a/lib/calculator.ts b/lib/calculator.ts index 96749f1..f7d8771 100644 --- a/lib/calculator.ts +++ b/lib/calculator.ts @@ -36,10 +36,4 @@ export const calcTotalPrice = (finalPrice: number|string, qty: number) => { interface ICalcTotalItem { qty: number; price: string|number; -} - -export const calcBenefit = (price: number|string, price_old: number|string|null) => { - if (!price_old) return null; - - return currency(price_old).subtract(price).format(); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/lib/price.ts b/lib/price.ts deleted file mode 100644 index 413cb9a..0000000 --- a/lib/price.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {IProductVariant} from 'boundless-api-client'; -import {calcBenefit} from './calculator'; -import currency from 'currency.js'; - -export const getLowestPrice = (variants: IProductVariant[]): IPrice => { - if (!variants || !variants.length) return { - price: 0, - price_old: null, - benefit: null - }; - - const [lowest] = [...variants].sort((el1, el2) => { - if (parseInt(el1.price) > parseInt(el2.price)) return 1; - if (parseInt(el1.price) < parseInt(el2.price)) return -1; - return 0; - }); - - return { - price: currency(lowest.price).format(), - price_old: lowest.price_old && currency(lowest.price_old).format() , - benefit: calcBenefit(lowest.price, lowest.price_old) - }; -}; - -export interface IPrice { - price: string|number; - price_old: string|number|null; - benefit: string|number|null; -} \ No newline at end of file diff --git a/pages/category/[slug].tsx b/pages/category/[slug].tsx index 62ff847..d4260cd 100644 --- a/pages/category/[slug].tsx +++ b/pages/category/[slug].tsx @@ -128,8 +128,6 @@ const fetchData = async (slug: string, params: TQuery) => { with_filter: 1 }); - params['per-page'] = 12; - const {collection, filteredQuery: productsQuery} = await fetchCollection(category.category_id, params); const out = { @@ -143,7 +141,12 @@ const fetchData = async (slug: string, params: TQuery) => { const fetchCollection = async (categoryId: number, params: TQuery) => { const filteredQuery = filterProductsQuery(params); - const collection = await apiClient.catalog.getProducts({category: [categoryId], sort: 'in_category,-in_stock,price', ...filteredQuery}); + const collection = await apiClient.catalog.getProducts({ + category: [categoryId], + sort: 'in_category,-in_stock,price', + ...filteredQuery, + ...{'per-page': 12} + }); return { filteredQuery, diff --git a/pages/index.tsx b/pages/index.tsx index 3b15ee4..c09d5b3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,7 @@ import {ICategory} from 'boundless-api-client/types/catalog/category'; import {IProduct} from 'boundless-api-client/types/catalog/product'; import {GetServerSideProps, InferGetServerSidePropsType} from 'next'; -import CategoryHomeMenu from '../components/category/CategoryHomeMenu'; +import CategoryHomeMenu from '../components/category/HomeMenu'; import ProductsList from '../components/ProductsList'; import MainLayout from '../layouts/Main'; import {apiClient} from '../lib/api'; diff --git a/redux/actions/cart.ts b/redux/actions/cart.ts index 4bbb09c..b258d67 100644 --- a/redux/actions/cart.ts +++ b/redux/actions/cart.ts @@ -59,10 +59,10 @@ export const addItem2Cart = (itemId: number, qty: number = 1, callToOrder: boole dispatch(setCartSubmitting(true)); const promise = apiClient.orders.addItemToCart(cartId, itemId, qty).then( - ({variants, product, actionRequired, cartTotal, added}) => { + ({product, actionRequired, cartTotal, added}) => { dispatch(setCartSubmitting(false)); - if (actionRequired === 'chooseVariant' && variants && product) { - dispatch(showVariantModal({product, variants})); + if (actionRequired === 'chooseVariant' && product) { + dispatch(showVariantModal({product})); } else if (cartTotal) { dispatch(setCartTotal(cartTotal)); if (callToOrder && added) dispatch(showCall2Order(added)); diff --git a/redux/reducers/cart.ts b/redux/reducers/cart.ts index e2f191b..2d012b1 100644 --- a/redux/reducers/cart.ts +++ b/redux/reducers/cart.ts @@ -1,5 +1,5 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; -import {ICartProduct, ICartTotal, IExtendedVariants, IVwItem} from 'boundless-api-client'; +import {ICartProduct, ICartTotal, IVwItem} from 'boundless-api-client'; export interface CartState { cartId: string|null; @@ -63,7 +63,6 @@ export const {setCartId, setCartLoading, setCartTotal, showVariantModal, hideVar export default cartSlice.reducer; export interface IVariantModalData { - variants?: IExtendedVariants; product?: ICartProduct; } export interface ICall2OrderData { diff --git a/styles/components/noImage.scss b/styles/components/noImage.scss index a06a257..c79e423 100644 --- a/styles/components/noImage.scss +++ b/styles/components/noImage.scss @@ -46,4 +46,10 @@ &.r-9-16 { padding-bottom: calc(16/9 * 100%); } + + &.bg-xs { + .no-image__bg { + background-size: 30px; + } + } } diff --git a/styles/components/product/variantAndBuy.scss b/styles/components/product/variantAndBuy.scss index c1f23ac..d1c92db 100644 --- a/styles/components/product/variantAndBuy.scss +++ b/styles/components/product/variantAndBuy.scss @@ -51,8 +51,8 @@ .variant-picker { background: #f2dede; border: 1px solid #ebccd1; - padding: 4px; border-radius: 4px; + padding: 4px; } } } @@ -71,17 +71,17 @@ } &__old { - display: inline-block; color: $text-muted; + display: inline-block; + font-size: 1.5rem; margin: 0 5px; text-decoration: line-through; - font-size: 1.5rem; } &__current { display: inline-block; - margin: 0 5px; font-size: 1.8rem; + margin: 0 5px; &.has-old { color: $front-highlighted-price-color; @@ -97,11 +97,11 @@ } &__2-cart { - margin: -5px -10px 1rem -10px; + align-items: flex-start; display: flex; - align-items: flex-start; flex-wrap: wrap; + margin: -5px -10px 1rem; } &__btns { @@ -111,9 +111,9 @@ } &__qty { - width: 130px; flex-shrink: 0; margin: 5px 10px; + width: 130px; .form-control { text-align: center; @@ -126,9 +126,9 @@ } &.out { + color: $text-muted; font-size: 1.5rem; font-weight: $front-font-weight-bold; - color: $text-muted; } } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index e1885f4..cbe548f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -655,7 +655,7 @@ bootstrap@^5.1.3: "boundless-api-client@ssh://git@github.com:kirill-zhirnov/boundless-api-client": version "0.0.1" - resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#b3448a520fe3af51674ec93633aa39c35c1fada3" + resolved "ssh://git@github.com:kirill-zhirnov/boundless-api-client#b7bb957f1170f95c3e6fac1cf7ae5d620985eece" dependencies: axios "^0.24.0" jsonwebtoken "^8.5.1" From 0a222c9c37515ce0a5c3ca2c62761c0e6c3beade Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Fri, 21 Jan 2022 19:31:35 +0300 Subject: [PATCH 006/133] aside menu and other improvements --- {public => assets}/logo.svg | 0 components/AsideMenu.tsx | 57 ++++++++++++++++++ components/Header.tsx | 21 ++++++- components/asideMenu/Backdrop.tsx | 17 ++++++ layouts/Main.tsx | 14 ++++- package.json | 2 + pages/category/[slug].tsx | 52 ++++++++--------- pages/index.tsx | 26 ++++----- pages/product/[slug].tsx | 46 +++++++-------- redux/reducers/asideMenu.ts | 22 +++++++ redux/store.ts | 2 + ...{defaultVariables.scss => _variables.scss} | 8 ++- styles/components/asideMenu.scss | 58 +++++++++++++++++++ styles/components/pageHeader.scss | 21 +++++++ styles/components/product.scss | 35 ----------- styles/components/products.scss | 6 +- styles/functions/_stripUnits.scss | 5 ++ styles/mixins/_fluidType.scss | 14 +++++ styles/styles.scss | 10 ++-- styles/themes/sample/page.scss | 4 -- yarn.lock | 10 ++++ 21 files changed, 314 insertions(+), 116 deletions(-) rename {public => assets}/logo.svg (100%) create mode 100644 components/AsideMenu.tsx create mode 100644 components/asideMenu/Backdrop.tsx create mode 100644 redux/reducers/asideMenu.ts rename styles/components/{defaultVariables.scss => _variables.scss} (85%) create mode 100644 styles/components/asideMenu.scss create mode 100644 styles/functions/_stripUnits.scss create mode 100644 styles/mixins/_fluidType.scss diff --git a/public/logo.svg b/assets/logo.svg similarity index 100% rename from public/logo.svg rename to assets/logo.svg diff --git a/components/AsideMenu.tsx b/components/AsideMenu.tsx new file mode 100644 index 0000000..b423af4 --- /dev/null +++ b/components/AsideMenu.tsx @@ -0,0 +1,57 @@ +import clsx from 'clsx'; +import {useAppDispatch, useAppSelector} from '../hooks/redux'; +import {RootState} from '../redux/store'; +import {disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks} from 'body-scroll-lock'; +import {useEffect, useRef} from 'react'; +import {setIsOpened} from '../redux/reducers/asideMenu'; + +export default function AsideMenu() { + const rootEl = useRef(null); + const isOpened = useAppSelector((state: RootState) => state.asideMenu.isOpened); + const dispatch = useAppDispatch(); + + const closeIfOpened = () => { + if (isOpened) { + dispatch(setIsOpened(false)); + } + }; + const onEscPressed = (e: KeyboardEvent) => { + if (e.defaultPrevented || !isOpened) { + return; + } + + if (e.key !== undefined && e.key === 'Escape') { + closeIfOpened(); + } else if (e.keyCode !== undefined && e.keyCode === 13) { + closeIfOpened(); + } + }; + + useEffect(() => { + if (rootEl.current) { + if (isOpened) { + disableBodyScroll(rootEl.current); + window.addEventListener('resize', closeIfOpened); + window.addEventListener('keydown', onEscPressed); + } else { + enableBodyScroll(rootEl.current); + window.removeEventListener('resize', closeIfOpened); + window.removeEventListener('keydown', onEscPressed); + } + } + + return () => { + clearAllBodyScrollLocks(); + window.removeEventListener('resize', closeIfOpened); + window.removeEventListener('keydown', onEscPressed); + }; + }, [isOpened, rootEl.current]); + + return ( + + ); +} \ No newline at end of file diff --git a/components/Header.tsx b/components/Header.tsx index d18e318..74afde7 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,9 +1,22 @@ +import {MouseEvent} from 'react'; import Link from 'next/link'; import CallToOrder from './header/CallToOrder'; import HeaderCart from './header/Cart'; import ChooseVariantModal from './header/ChooseVariantModal'; +import logoImg from '../assets/logo.svg'; +import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {useAppDispatch} from '../hooks/redux'; +import {setIsOpened} from '../redux/reducers/asideMenu'; export default function Header() { + const dispatch = useAppDispatch(); + + const onHamburgerBtnClicked = (e: MouseEvent) => { + e.preventDefault(); + dispatch(setIsOpened(true)); + }; + return (
@@ -11,11 +24,17 @@ export default function Header() { +
diff --git a/components/asideMenu/Backdrop.tsx b/components/asideMenu/Backdrop.tsx new file mode 100644 index 0000000..9a4e4ec --- /dev/null +++ b/components/asideMenu/Backdrop.tsx @@ -0,0 +1,17 @@ +import {useAppDispatch, useAppSelector} from '../../hooks/redux'; +import {RootState} from '../../redux/store'; +import clsx from 'clsx'; +import {setIsOpened} from '../../redux/reducers/asideMenu'; + +export default function AsideBackdrop() { + const asideIsOpened = useAppSelector((state: RootState) => state.asideMenu.isOpened); + const dispatch = useAppDispatch(); + + const onClicked = () => dispatch(setIsOpened(false)); + + return ( +
+ ); +} \ No newline at end of file diff --git a/layouts/Main.tsx b/layouts/Main.tsx index e7d2e2c..c5ae5d3 100644 --- a/layouts/Main.tsx +++ b/layouts/Main.tsx @@ -1,3 +1,4 @@ +import dynamic from 'next/dynamic'; import {ReactNode, useEffect} from 'react'; import Head from 'next/head'; import Header from '../components/Header'; @@ -5,9 +6,13 @@ import Footer from '../components/Footer'; import LoadingLine from '../components/LoadingLine'; import AlertWidget from '../components/Alert'; import {useRouter} from 'next/router'; -import {useAppDispatch} from '../hooks/redux'; +import {useAppDispatch, useAppSelector} from '../hooks/redux'; import {endRouting, startRouting} from '../redux/reducers/app'; import {hideCall2Order} from '../redux/reducers/cart'; +import {RootState} from '../redux/store'; +import clsx from 'clsx'; +const AsideMenu = dynamic(() => import('../components/AsideMenu'), {ssr: false}); +import AsideBackdrop from '../components/asideMenu/Backdrop'; const shopBaseUrl = process.env.BOUNDLESS_BASE_URL || ''; @@ -15,6 +20,7 @@ export default function MainLayout({children, title, metaData}: IMainLayoutProps const {canonicalUrl, imgUrl, description} = metaData || {}; const router = useRouter(); const dispatch = useAppDispatch(); + const asideIsOpened = useAppSelector((state: RootState) => state.asideMenu.isOpened); useEffect(() => { const handleStart = () => { @@ -60,13 +66,15 @@ export default function MainLayout({children, title, metaData}: IMainLayoutProps -
+
-
+
{children}
+
+ ); } diff --git a/package.json b/package.json index 6e0b237..a00c065 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/lodash": "^4.14.178", "@types/nprogress": "^0.2.0", "@types/react-redux": "^7.1.21", + "body-scroll-lock": "^4.0.0-beta.0", "bootstrap": "^5.1.3", "boundless-api-client": "ssh://git@github.com:kirill-zhirnov/boundless-api-client", "clsx": "^1.1.1", @@ -38,6 +39,7 @@ "typescript": "^4.5.4" }, "devDependencies": { + "@types/body-scroll-lock": "^3.1.0", "@types/lodash": "^4.14.178", "@types/node": "^17.0.0", "@types/qs": "^6.9.7", diff --git a/pages/category/[slug].tsx b/pages/category/[slug].tsx index d4260cd..dc74a94 100644 --- a/pages/category/[slug].tsx +++ b/pages/category/[slug].tsx @@ -46,35 +46,33 @@ export default function CategoryPage({errorCode, data}: InferGetServerSidePropsT const title = category?.text?.custom_header || category?.text?.title; return ( - <> - -
-
-
- - -
-
-

{title}

- - {category.text?.description_top && -
- } - - {collection && <> - - - - } - {category.text?.description_bottom &&
} -
+ +
+
+
+ +
+
+ +

{title}

+ {category.text?.description_top && +
+ } + + {collection && <> + + + + } + {category.text?.description_bottom &&
} +
- - +
+
); } diff --git a/pages/index.tsx b/pages/index.tsx index c09d5b3..c27e5ca 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -7,22 +7,20 @@ import MainLayout from '../layouts/Main'; import {apiClient} from '../lib/api'; export default function IndexPage({categoryTree, products}: InferGetServerSidePropsType) { - return ( - <> - -
-
- -
- -
-
+ +
+
+ +
+

Boundless store

+ +
- - +
+
); } diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 65e1a8c..223c628 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -36,33 +36,31 @@ export default function ProductPage({data}: InferGetStaticPropsType - -
- {resolvedParents && } -
-
-
-

{product.text.title}

- - -
-
- -
- -

Shipping

- We ship ASAP! -
+ +
+ {resolvedParents && } +
+
+
+

{product.text.title}

+ + +
+
+ +
+ +

Shipping

+ We ship ASAP!
- {product.text.description &&
} -
+ {product.text.description &&
} +
- - +
+
); } diff --git a/redux/reducers/asideMenu.ts b/redux/reducers/asideMenu.ts new file mode 100644 index 0000000..8ed1b14 --- /dev/null +++ b/redux/reducers/asideMenu.ts @@ -0,0 +1,22 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; + +const initialState: IAsideMenuState = { + isOpened: false +}; + +const asideMenuSlice = createSlice({ + name: 'asideMenu', + initialState, + reducers: { + setIsOpened(state, action: PayloadAction) { + state.isOpened = action.payload; + } + } +}); + +export const {setIsOpened} = asideMenuSlice.actions; +export default asideMenuSlice.reducer; + +export interface IAsideMenuState { + isOpened: boolean +} \ No newline at end of file diff --git a/redux/store.ts b/redux/store.ts index a52e5f5..576beba 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -3,6 +3,7 @@ import xhrReducers from './reducers/xhr'; import cartReducers from './reducers/cart'; import alertReducers from './reducers/alert'; import appReducers from './reducers/app'; +import asideMenuReducers from './reducers/asideMenu'; export const store = configureStore({ reducer: { @@ -10,6 +11,7 @@ export const store = configureStore({ cart: cartReducers, alert: alertReducers, app: appReducers, + asideMenu: asideMenuReducers }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/styles/components/defaultVariables.scss b/styles/components/_variables.scss similarity index 85% rename from styles/components/defaultVariables.scss rename to styles/components/_variables.scss index 835f7b5..1a57f6a 100644 --- a/styles/components/defaultVariables.scss +++ b/styles/components/_variables.scss @@ -14,7 +14,8 @@ $front-highlight-link-color: $theme-color; $front-highlighted-price-color: #dc3545; $zindex-call-to-order: 1031; -$zindex-nprogress: 1032; +$zindex-nprogress: 1033; +$zindex-collapsed-background: 1032; $front-bg-color: #f8f8f8; @@ -26,3 +27,8 @@ $front-products-border-color: #cfd3da; $front-error-color: #a94442; $front-hr-color: #eee; + +$aside-width: ( + 'xxs': 290px, + 'xs': 430px +); diff --git a/styles/components/asideMenu.scss b/styles/components/asideMenu.scss new file mode 100644 index 0000000..a705cb1 --- /dev/null +++ b/styles/components/asideMenu.scss @@ -0,0 +1,58 @@ +.page-layout { + position: relative; + transition: transform 0.4s ease; + + &_aside-opened { + box-shadow: 5px 0 10px 0 rgba(0, 0, 0, 0.75); + min-height: 100vh; + transform: translate3d((map-get($aside-width, 'xxs') * -1), 0, 0); + z-index: 2; + + @include media-breakpoint-up(sm) { + transform: translate3d((map-get($aside-width, 'xs') * -1), 0, 0); + } + } +} + +.aside-backdrop { + background: rgba(0, 0, 0, 0.6); + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition: opacity 1s ease; + visibility: hidden; + z-index: $zindex-collapsed-background; + + &_visible { + opacity: 1; + visibility: visible; + } +} + +.aside-menu { + background: #423e3e; + bottom: 0; + max-width: 100vw; + overflow-x: hidden; + overflow-y: auto; + position: fixed; + right: 0; + top: 0; + transform: translate3d(map-get($aside-width, 'xxs'), 0, 0); + transition: transform 0.4s ease; + visibility: hidden; + width: map-get($aside-width, 'xxs'); + + @include media-breakpoint-up(sm) { + transform: translate3d(map-get($aside-width, 'xs'), 0, 0); + width: map-get($aside-width, 'xs'); + } + + &_visible { + transform: translate3d(0, 0, 0); + visibility: visible; + } +} diff --git a/styles/components/pageHeader.scss b/styles/components/pageHeader.scss index 3672077..d75de7b 100644 --- a/styles/components/pageHeader.scss +++ b/styles/components/pageHeader.scss @@ -1,5 +1,26 @@ .page-header { + position: relative; + text-align: center; + &_h1 { + @include font-size(2rem); + font-weight: $front-font-weight-bold; + } + + &_m-h1 { + margin-bottom: 4rem; + margin-top: 2rem; + } + &::after { + background: $theme-color; + bottom: -25px; + content: ''; + display: block; + height: 4px; + left: 50%; + margin-left: -30px; + position: absolute; + width: 60px; } } diff --git a/styles/components/product.scss b/styles/components/product.scss index 34a2d98..9c061af 100644 --- a/styles/components/product.scss +++ b/styles/components/product.scss @@ -6,41 +6,6 @@ max-width: 800px; } -//.variant-picker { -// padding-bottom: 12px; -//} -// -//.variant-picker + .price-and-buy { -// border-top: 1px solid #eee; -//} - -/* -.price-and-buy { - border-bottom: 1px solid #eee; - margin-bottom: 20px; - padding: 20px 0; - - .prices { - .price { - font-size: 26px; - } - - &.with-old .price { - color: $front-highlighted-price-color; - } - } - - .price-old { - color: #777; - font-size: 22px; - } - - .benefit-price span { - color: $front-highlighted-price-color; - } -} - */ - .modal-price { .prices { margin-bottom: 0; diff --git a/styles/components/products.scss b/styles/components/products.scss index 1f9b9c8..17d4ad5 100644 --- a/styles/components/products.scss +++ b/styles/components/products.scss @@ -17,14 +17,14 @@ $products-gutter: 10px; position: relative; transition: all ease-in-out 0.35s; - width: calc((100% - (4 * $products-gutter)) / 2); + width: calc((100% - (4 * #{$products-gutter})) / 2); @include media-breakpoint-up(lg) { - width: calc((100% - (6 * $products-gutter)) / 3); + width: calc((100% - (6 * #{$products-gutter})) / 3); } @include media-breakpoint-up(xl) { - width: calc((100% - (8 * $products-gutter)) / 4); + width: calc((100% - (8 * #{$products-gutter})) / 4); } &:hover { diff --git a/styles/functions/_stripUnits.scss b/styles/functions/_stripUnits.scss new file mode 100644 index 0000000..8c00d05 --- /dev/null +++ b/styles/functions/_stripUnits.scss @@ -0,0 +1,5 @@ +@use 'sass:math'; + +@function strip-units($number) { + @return math.div($number, $number * 0 + 1); +} diff --git a/styles/mixins/_fluidType.scss b/styles/mixins/_fluidType.scss new file mode 100644 index 0000000..81c3cf1 --- /dev/null +++ b/styles/mixins/_fluidType.scss @@ -0,0 +1,14 @@ +@mixin fluid-type($property, $min-screen-width, $max-screen-width, $min-size, $max-size) { + #{$property}: $min-size; + + $property-step: strip-units($max-size) - strip-units($min-size); + $screen-step: strip-units($max-screen-width) - strip-units($min-screen-width); + + @media screen and (min-width: $min-screen-width) { + #{$property}: calc(#{$min-size} + #{$property-step} * ((100vw - #{$min-screen-width}) / #{$screen-step})); + } + + @media screen and (min-width: $max-screen-width) { + #{$property}: $max-size; + } +} diff --git a/styles/styles.scss b/styles/styles.scss index 2196e95..0201858 100644 --- a/styles/styles.scss +++ b/styles/styles.scss @@ -1,10 +1,11 @@ -@use 'sass:math'; - @import '~bootstrap'; $theme-color: #c490e4; -@import 'components/defaultVariables'; +@import 'components/variables'; + +//@import 'functions/stripUnits'; +//@import 'mixins/fluidType';s @import 'themes/sample/basic'; @import 'themes/sample/page'; @@ -14,7 +15,7 @@ $theme-color: #c490e4; @import './components/buttons'; @import './components/products'; @import './components/product'; -//@import './components/category'; +@import './components/pageHeader'; @import './components/nprogress'; @import './components/breadcrumb'; @import './components/cart'; @@ -22,3 +23,4 @@ $theme-color: #c490e4; @import './components/alert'; @import './components/sort'; @import './components/categoriesSidebar'; +@import './components/asideMenu'; diff --git a/styles/themes/sample/page.scss b/styles/themes/sample/page.scss index e1a8068..15e34b3 100644 --- a/styles/themes/sample/page.scss +++ b/styles/themes/sample/page.scss @@ -3,7 +3,3 @@ body .page { flex-direction: column; min-height: 100vh; } - -body h2 { - color: #ba86da; -} diff --git a/yarn.lock b/yarn.lock index cbe548f..aa2b80a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,6 +293,11 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== +"@types/body-scroll-lock@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.0.tgz#435f6abf682bf58640e1c2ee5978320b891970e7" + integrity sha512-3owAC4iJub5WPqRhxd8INarF2bWeQq1yQHBgYhN0XLBJMpd5ED10RrJ3aKiAwlTyL5wK7RkBD4SZUQz2AAAMdA== + "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -648,6 +653,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +body-scroll-lock@^4.0.0-beta.0: + version "4.0.0-beta.0" + resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz#4f78789d10e6388115c0460cd6d7d4dd2bbc4f7e" + integrity sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ== + bootstrap@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" From 0d98444bc0c2908ca2fcd6bdd10a090f14ed704f Mon Sep 17 00:00:00 2001 From: igorlazicT Date: Sat, 22 Jan 2022 14:21:20 +0300 Subject: [PATCH 007/133] added generic menu to redux. --- components/category/HomeMenu.tsx | 28 +++++++++++++----- components/category/Sidebar.tsx | 9 ++++-- lib/imgs.ts | 16 +++++++++-- lib/menu.ts | 49 ++++++++++++++++++++++++++++++++ pages/category/[slug].tsx | 43 +++++++++++++++------------- pages/index.tsx | 18 ++++++++++-- pages/product/[slug].tsx | 18 ++++++++++-- redux/reducers/menus.ts | 40 ++++++++++++++++++++++++++ redux/store.ts | 4 ++- 9 files changed, 186 insertions(+), 39 deletions(-) create mode 100644 lib/menu.ts create mode 100644 redux/reducers/menus.ts diff --git a/components/category/HomeMenu.tsx b/components/category/HomeMenu.tsx index 2471367..b3ad019 100644 --- a/components/category/HomeMenu.tsx +++ b/components/category/HomeMenu.tsx @@ -9,21 +9,35 @@ export default function CategoryHomeMenu({categoryTree}: {categoryTree: ICategor