Skip to content

Commit

Permalink
generic variants picker
Browse files Browse the repository at this point in the history
  • Loading branch information
mister-teddy committed Apr 18, 2023
1 parent f7c0ad8 commit 3b5a72b
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 58 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zaui-coffee",
"private": true,
"version": "1.0.0",
"version": "1.1.0",
"description": "ZaUI Coffee",
"repository": "",
"license": "UNLICENSED",
Expand Down Expand Up @@ -48,4 +48,4 @@
"vite": "^2.6.14",
"vite-tsconfig-paths": "^4.0.5"
}
}
}
11 changes: 9 additions & 2 deletions src/components/display/final-price.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import React, { FC, useMemo } from "react";
import { SelectedOptions } from "types/cart";
import { Product } from "types/product";
import { calcFinalPrice } from "utils/product";
import { DisplayPrice } from "./price";

export const FinalPrice: FC<{ children: Product }> = ({ children }) => {
const finalPrice = useMemo(() => calcFinalPrice(children), [children]);
export const FinalPrice: FC<{
children: Product;
options?: SelectedOptions;
}> = ({ children, options }) => {
const finalPrice = useMemo(
() => calcFinalPrice(children, options),
[children, options]
);
return <DisplayPrice>{finalPrice}</DisplayPrice>;
};
25 changes: 25 additions & 0 deletions src/components/display/price-change.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { FC, useMemo } from "react";
import { Option, Product } from "types/product";
import { calcFinalPrice } from "utils/product";
import { DisplayPrice } from "./price";

export const DisplayPriceChange: FC<{ children: Product; option: Option }> = ({
children,
option,
}) => {
const changes = useMemo(
() =>
option.priceChange
? option.priceChange.type === "fixed"
? option.priceChange.amount
: children.price * option.priceChange.percent
: 0,
[children, option]
);
return (
<>
{changes > 0 && "+"}
<DisplayPrice>{changes}</DisplayPrice>
</>
);
};
42 changes: 42 additions & 0 deletions src/components/display/selected-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { FC, useMemo } from "react";
import { SelectedOptions } from "types/cart";
import { Product } from "types/product";

export const DisplaySelectedOptions: FC<{
children: Product;
options: SelectedOptions;
}> = ({ children, options }) => {
const description = useMemo(() => {
let variants: string[] = [];
if (children.variants) {
const selectedVariants = Object.keys(options);
children.variants
.filter((v) => selectedVariants.includes(v.key))
.forEach((variant) => {
if (variant.type === "single") {
const selectedOption = variant.options.find(
(o) => o.key === options[variant.key]
);
if (selectedOption) {
variants.push(
`${variant.label || variant.key}: ${
selectedOption.label || selectedOption.key
}`
);
}
} else {
const selectedOptions = variant.options.filter((o) =>
options[variant.key].includes(o.key)
);
variants.push(
`${variant.label || variant.key}: ${selectedOptions
.map((o) => o.label || o.key)
.join(", ")}`
);
}
});
}
return variants.join(". ");
}, [children]);
return <>{description}</>;
};
4 changes: 3 additions & 1 deletion src/components/list-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface ListRendererProps<T> {
items: T[];
renderLeft: (item: T) => ReactNode;
renderRight: (item: T) => ReactNode;
renderKey?: (item: T) => string;
onClick?: (item: T) => void;
noDivider?: boolean;
}
Expand All @@ -17,6 +18,7 @@ export function ListRenderer<T>({
limit,
renderLeft,
renderRight,
renderKey,
onClick,
noDivider,
}: ListRendererProps<T>) {
Expand All @@ -31,7 +33,7 @@ export function ListRenderer<T>({
<Box>
{(isCollapsed ? collapsedItems : items).map((item, i, list) => (
<div
key={i}
key={renderKey ? renderKey(item) : i}
onClick={() => onClick?.(item)}
className="flex space-x-4 p-4 last:pb-0"
>
Expand Down
40 changes: 40 additions & 0 deletions src/components/product/multiple-option-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DisplayPriceChange } from "components/display/price-change";
import React, { FC } from "react";
import { MultipleOptionVariant, Product } from "types/product";
import { Box, Checkbox, Text } from "zmp-ui";

export const MultipleOptionPicker: FC<{
product: Product;
variant: MultipleOptionVariant;
value: string[];
onChange: (value: string[]) => void;
}> = ({ product, variant, value, onChange }) => {
return (
<Box my={8} className="space-y-2">
<Text.Title size="small">{variant.label}</Text.Title>
<Checkbox.Group
className="flex flex-col space-y-2"
name={variant.key}
options={variant.options.map((option) => ({
className: "last-of-type:mr-2",
value: option.key,
label: (
<div className="w-full">
<span className="flex-1">{option.label}</span>
<span className="absolute right-0">
<DisplayPriceChange option={option}>
{product}
</DisplayPriceChange>
</span>
</div>
) as any,
}))}
value={value}
defaultValue={value}
onChange={(selectedOptions: string[]) => {
onChange(selectedOptions);
}}
/>
</Box>
);
};
98 changes: 80 additions & 18 deletions src/components/product/picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,51 @@ import React, { FC, ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useSetRecoilState } from "recoil";
import { cartState } from "state";
import { SelectedOptions } from "types/cart";
import { Product } from "types/product";
import { isIdentical } from "utils/product";
import { Box, Button, Text } from "zmp-ui";
import { MultipleOptionPicker } from "./multiple-option-picker";
import { QuantityPicker } from "./quantity-picker";
import { Size, SizePicker } from "./size-picker";
import { SingleOptionPicker } from "./single-option-picker";

export interface ProductPickerProps {
product?: Product;
selected?: {
size: Size;
options: SelectedOptions;
quantity: number;
};
children: (methods: { open: () => void; close: () => void }) => ReactNode;
}

function getDefaultOptions(product?: Product) {
if (product && product.variants) {
return product.variants.reduce(
(options, variant) =>
Object.assign(options, {
[variant.key]: variant.default,
}),
{}
);
}
return {};
}

export const ProductPicker: FC<ProductPickerProps> = ({
children,
product,
selected,
}) => {
const [visible, setVisible] = useState(false);
const [size, setSize] = useState<Size>("M");
const [options, setOptions] = useState<SelectedOptions>(
selected ? selected.options : getDefaultOptions(product)
);
const [quantity, setQuantity] = useState(1);
const setCart = useSetRecoilState(cartState);

useEffect(() => {
if (selected) {
setSize(selected.size);
setOptions(selected.options);
setQuantity(selected.quantity);
}
}, [selected]);
Expand All @@ -41,23 +59,35 @@ export const ProductPicker: FC<ProductPickerProps> = ({
let res = [...cart];
if (selected) {
// updating an existing cart item, including quantity and size, or remove it if new quantity is 0
const existed = cart.find(
const editing = cart.find(
(item) =>
item.product.id === product.id && item.size === selected.size
item.product.id === product.id &&
isIdentical(item.options, selected.options)
)!;
if (quantity > 0) {
res.splice(cart.indexOf(existed), 1, {
...existed,
size,
quantity,
});
if (quantity === 0) {
res.splice(cart.indexOf(editing), 1);
} else {
res.splice(cart.indexOf(existed), 1);
const existed = cart.find(
(item, i) =>
i !== cart.indexOf(editing) &&
item.product.id === product.id &&
isIdentical(item.options, options)
)!;
res.splice(cart.indexOf(editing), 1, {
...editing,
options,
quantity: existed ? existed.quantity + quantity : quantity,
});
if (existed) {
res.splice(cart.indexOf(existed), 1);
}
}
} else {
// adding new item to cart, or merging if it already existed before
const existed = cart.find(
(item) => item.product.id === product.id && item.size === size
(item) =>
item.product.id === product.id &&
isIdentical(item.options, options)
);
if (existed) {
res.splice(cart.indexOf(existed), 1, {
Expand All @@ -67,7 +97,7 @@ export const ProductPicker: FC<ProductPickerProps> = ({
} else {
res = res.concat({
product,
size,
options,
quantity,
});
}
Expand All @@ -90,7 +120,7 @@ export const ProductPicker: FC<ProductPickerProps> = ({
<Box className="space-y-2">
<Text.Title>{product.name}</Text.Title>
<Text>
<FinalPrice>{product}</FinalPrice>
<FinalPrice options={options}>{product}</FinalPrice>
</Text>
<Text>
<div
Expand All @@ -101,7 +131,35 @@ export const ProductPicker: FC<ProductPickerProps> = ({
</Text>
</Box>
<Box className="space-y-5">
<SizePicker value={size} onChange={setSize} />
{product.variants &&
product.variants.map((variant) =>
variant.type === "single" ? (
<SingleOptionPicker
key={variant.key}
variant={variant}
value={options[variant.key] as string}
onChange={(selectedOption) =>
setOptions((prevOptions) => ({
...prevOptions,
[variant.key]: selectedOption,
}))
}
/>
) : (
<MultipleOptionPicker
key={variant.key}
product={product}
variant={variant}
value={options[variant.key] as string[]}
onChange={(selectedOption) =>
setOptions((prevOptions) => ({
...prevOptions,
[variant.key]: selectedOption,
}))
}
/>
)
)}
<QuantityPicker value={quantity} onChange={setQuantity} />
{selected ? (
<Button
Expand All @@ -110,7 +168,11 @@ export const ProductPicker: FC<ProductPickerProps> = ({
fullWidth
onClick={addToCart}
>
{quantity > 0 ? "Thêm vào giỏ hàng" : "Xoá"}
{quantity > 0
? selected
? "Cập nhật giỏ hàng"
: "Thêm vào giỏ hàng"
: "Xoá"}
</Button>
) : (
<Button
Expand Down
27 changes: 27 additions & 0 deletions src/components/product/single-option-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FC } from "react";
import { Variant } from "types/product";
import { Box, Radio, Text } from "zmp-ui";

export const SingleOptionPicker: FC<{
variant: Variant;
value: string;
onChange: (value: string) => void;
}> = ({ variant, value, onChange }) => {
return (
<Box my={8} className="space-y-2">
<Text.Title size="small">{variant.label}</Text.Title>
<Radio.Group
className="flex-1 grid grid-cols-3 justify-between"
name={variant.key}
options={variant.options.map((option) => ({
value: option.key,
label: option.label,
}))}
value={value}
onChange={(selectedOption: string) => {
onChange(selectedOption);
}}
/>
</Box>
);
};
25 changes: 0 additions & 25 deletions src/components/product/size-picker.tsx

This file was deleted.

Loading

0 comments on commit 3b5a72b

Please sign in to comment.