add area carpet fixture. Add carpet roll calculator test. refactor carpet roll as own component. add icons.
This commit is contained in:
@ -1,17 +1,12 @@
|
||||
import { area_t } from "@/lib/dimensions";
|
||||
import { Product } from "@/lib/product";
|
||||
import convert, { Area, Length } from "convert";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
export type AreaRugTagProps = {
|
||||
dimensions: area_t,
|
||||
price_per_area: {
|
||||
price: number,
|
||||
per: {
|
||||
n: number,
|
||||
u: Area,
|
||||
}
|
||||
},
|
||||
product: Product,
|
||||
date?: Dayjs
|
||||
currencySymbol?: string
|
||||
};
|
||||
@ -19,17 +14,17 @@ export type AreaRugTagProps = {
|
||||
export const AreaRugTag = (props: AreaRugTagProps) => {
|
||||
const date = props.date || dayjs();
|
||||
const square = props.dimensions.l * props.dimensions.w;
|
||||
const areaUnits = `square ${props.dimensions.u}`;
|
||||
const square2 = convert(square, areaUnits as Area).to(props.price_per_area.per.u)
|
||||
const price = (square2 / props.price_per_area.per.n) * props.price_per_area.price;
|
||||
const areaUnits = `sq ${props.dimensions.u}`;
|
||||
const square2 = convert(square, areaUnits as Area).to("sq " + props.product.dimensions.u as Area)
|
||||
const price = (square2 / props.product.pricePerUnit) * props.product.pricePerUnit;
|
||||
const sPrice = price.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
const currencySymbol = props.currencySymbol || "$";
|
||||
return (
|
||||
<View style={styles.component}>
|
||||
<Text aria-label="area rug dimensions" style={styles.dimensions}>{props.dimensions.l} x {props.dimensions.w}</Text>
|
||||
<View aria-label="area rug tag" style={styles.component}>
|
||||
<Text aria-label="area rug dimensions" style={styles.dimensions}>{Math.round(props.dimensions.l)} x {Math.round(props.dimensions.w)}</Text>
|
||||
<Text aria-label="area rug price" style={styles.price}>{currencySymbol} {sPrice}</Text>
|
||||
<Text aria-label="area rug date" style={styles.date}>{date.format("YYYY/MM/DD")}</Text>
|
||||
<Text aria-label="this week's color" style={styles.tagColor}>[Curent Tag Color]</Text>
|
||||
|
106
components/CarpetRollCalculator.tsx
Normal file
106
components/CarpetRollCalculator.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View, Text, TextInput, Button, StyleSheet } from "react-native";
|
||||
import {
|
||||
productPriceFor,
|
||||
priceDisplay,
|
||||
pricePerUnitDisplay,
|
||||
Product,
|
||||
} from "@/lib/product";
|
||||
import { selectProducts } from "@/features/product/productSlice";
|
||||
import { area_t, diameterToLength, length_t } from "@/lib/dimensions";
|
||||
import { useAppSelector } from "../app/store";
|
||||
import { AreaRugTag } from "@/components/AreaRugTag";
|
||||
import { Length } from "convert";
|
||||
import ProductList from "@/components/ProductList";
|
||||
|
||||
const DEFAULT_UNIT: Length = "ft";
|
||||
|
||||
export const CarpetRollCalculator = () => {
|
||||
const products = useAppSelector(selectProducts);
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [outerDiameter, setOuterDiameter] = useState<length_t>({
|
||||
l: 0,
|
||||
u: DEFAULT_UNIT,
|
||||
});
|
||||
const [innerDiameter, setInnerDiameter] = useState<length_t>({
|
||||
l: 0,
|
||||
u: DEFAULT_UNIT,
|
||||
});
|
||||
const [numRings, setNumRings] = useState(0);
|
||||
const [price, setPrice] = useState(0);
|
||||
const [rugDimensions, setRugDimensions] = useState<area_t>({
|
||||
u: DEFAULT_UNIT,
|
||||
w: 0,
|
||||
l: 0,
|
||||
});
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
const [units, setUnits] = useState<Length>(DEFAULT_UNIT);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`recalculating...`);
|
||||
const dimens = {
|
||||
l: diameterToLength(outerDiameter, innerDiameter, numRings).l || 0.0,
|
||||
w: width || selectedProduct?.dimensions.l || 0.0,
|
||||
u: units || selectedProduct?.dimensions.u || "ft",
|
||||
};
|
||||
console.dir(dimens);
|
||||
setRugDimensions(dimens);
|
||||
}, [outerDiameter, innerDiameter, width, numRings, selectedProduct, units]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{selectedProduct && (
|
||||
<AreaRugTag dimensions={rugDimensions} product={selectedProduct} />
|
||||
)}
|
||||
<View>
|
||||
<Text>Length Calculation</Text>
|
||||
<View>
|
||||
<Text>Outer Diameter:</Text>
|
||||
<TextInput
|
||||
aria-label="outer diameter"
|
||||
onChangeText={(text) =>
|
||||
setOuterDiameter({ l: Number(text), u: units })
|
||||
}
|
||||
/>
|
||||
<Text>Inner Diameter:</Text>
|
||||
<TextInput
|
||||
aria-label="inner diameter"
|
||||
onChangeText={(text) =>
|
||||
setInnerDiameter({ l: Number(text), u: units })
|
||||
}
|
||||
/>
|
||||
<Text>Number of rings:</Text>
|
||||
<TextInput
|
||||
aria-label="number of rings"
|
||||
onChangeText={(text) => setNumRings(Number(text))}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text>Width:</Text>
|
||||
<TextInput
|
||||
aria-label="width"
|
||||
onChangeText={(text) => setWidth(Number(text))}
|
||||
/>
|
||||
</View>
|
||||
<Text>Price: {priceDisplay(price)}</Text>
|
||||
<Text>
|
||||
{selectedProduct ? pricePerUnitDisplay(selectedProduct) : "0.00"}
|
||||
</Text>
|
||||
<View style={styles.container}>
|
||||
<ProductList onProductSelected={setSelectedProduct} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default CarpetRollCalculator;
|
@ -1,22 +1,26 @@
|
||||
import { ScrollView, StyleSheet } from "react-native";
|
||||
import { ProductTile } from "./ProductTile";
|
||||
import { Product } from "@/lib/product";
|
||||
import { Product, product_type_t } from "@/lib/product";
|
||||
import { useState } from "react";
|
||||
import { selectProducts } from "@/features/product/productSlice";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
|
||||
export type ProductSelectionProps = {
|
||||
onProductSelected?: (product: Product) => any;
|
||||
productType?: product_type_t;
|
||||
};
|
||||
|
||||
export default function ProductList({
|
||||
productType,
|
||||
onProductSelected,
|
||||
}: ProductSelectionProps) {
|
||||
const [activeProduct, setActiveProduct] = useState(null as null | Product);
|
||||
const products = useAppSelector(selectProducts).filter(p => !!p).filter((p) => {
|
||||
console.dir(p);
|
||||
return !!p.dimensions;
|
||||
});
|
||||
const products = useAppSelector(selectProducts)
|
||||
.filter((p) => !!p)
|
||||
.filter((p: Product) => productType ? p.type === productType : true)
|
||||
.filter((p) => {
|
||||
return !!p.dimensions;
|
||||
});
|
||||
|
||||
function doOnProductSelected(product: Product) {
|
||||
setActiveProduct(product);
|
||||
|
@ -28,6 +28,7 @@ export function ProductTile ({product, onProductSelected, isActive} : ProductTil
|
||||
return (
|
||||
|
||||
<TouchableHighlight
|
||||
aria-label={`product ${product.id}`}
|
||||
style={styles[k].highlight}
|
||||
onPress={() => onProductSelected && onProductSelected(product)}>
|
||||
<Text style={styles[k].text}>{product.attributes?.name || `Product ${product.id}`} ({priceDisplay})</Text>
|
||||
|
@ -1,21 +1,23 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
import { AreaRugTag } from '@/components/AreaRugTag';
|
||||
import { area_t } from '@/lib/dimensions';
|
||||
import dayjs from 'dayjs';
|
||||
import { Area } from 'convert';
|
||||
|
||||
import initialProducts from '@/__fixtures__/initialProducts';
|
||||
import { Product } from '@/lib/product';
|
||||
|
||||
describe('AreaRugTag', () => {
|
||||
it('renders correctly with dimensions, price per area, date and currency symbol', () => {
|
||||
const dimensions: area_t = { l: 10, w: 20, u: 'foot' };
|
||||
const pricePerArea = { price: 100, per: { n: 1, u: 'square foot' as Area } };
|
||||
const dimensions: area_t = { l: 10, w: 20, u: 'ft' };
|
||||
const date = dayjs();
|
||||
const currencySymbol = '$';
|
||||
|
||||
const product = initialProducts.find(p => "area_rug" === p.type) as Product;
|
||||
|
||||
render(
|
||||
<AreaRugTag
|
||||
dimensions={dimensions}
|
||||
price_per_area={pricePerArea}
|
||||
product={product}
|
||||
date={date}
|
||||
currencySymbol={currencySymbol}
|
||||
/>
|
||||
|
44
components/__tests__/CarpetRollCalculator-test.tsx
Normal file
44
components/__tests__/CarpetRollCalculator-test.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { render, fireEvent, screen, within } from "@testing-library/react-native";
|
||||
import CarpetRollCalculator from "@/components/CarpetRollCalculator";
|
||||
import { renderWithProviders } from "@/lib/rendering";
|
||||
|
||||
import allProducts from "@/__fixtures__/initialProducts";
|
||||
import { Product, pricePerUnitDisplay } from "@/lib/product";
|
||||
import initialProducts from "@/__fixtures__/initialProducts";
|
||||
|
||||
const areaRugProducts = allProducts.filter((p) => "area_rug" === p.type);
|
||||
|
||||
describe("CarpetRollCalculator", () => {
|
||||
it("should render correctly", () => {
|
||||
renderWithProviders(<CarpetRollCalculator />, {
|
||||
products: initialProducts,
|
||||
});
|
||||
|
||||
const areaRug = initialProducts.find(p => p.type === 'area_rug') as Product;
|
||||
const areaRugLabel = `product ${areaRug.id}`;
|
||||
fireEvent.press(screen.getByLabelText(areaRugLabel));
|
||||
|
||||
// Test the interaction with the width input
|
||||
const widthInput = screen.getByLabelText("width");
|
||||
fireEvent.changeText(widthInput, "10");
|
||||
|
||||
// Test the interaction with the outer diameter input
|
||||
const outerDiameterInput = screen.getByLabelText("outer diameter");
|
||||
fireEvent.changeText(outerDiameterInput, "3");
|
||||
|
||||
// Test the interaction with the inner diameter input
|
||||
const innerDiameterInput = screen.getByLabelText("inner diameter");
|
||||
fireEvent.changeText(innerDiameterInput, "1");
|
||||
|
||||
// Test the interaction with the number of rings input
|
||||
const numRingsInput = screen.getByLabelText("number of rings");
|
||||
fireEvent.changeText(numRingsInput, "5");
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
// Test the interaction with the price display
|
||||
const {getByText} = within(screen.getByLabelText("area rug price"));
|
||||
expect(getByText(/\$.*58.*\..*19.*/)).toBeTruthy();
|
||||
});
|
||||
});
|
@ -2,34 +2,27 @@ import { render, fireEvent, screen, act, within } from '@testing-library/react-n
|
||||
import { Provider } from 'react-redux';
|
||||
import ProductCalculatorSelector from '@/components/ProductCalculatorSelector';
|
||||
import { renderWithProviders } from '@/lib/rendering';
|
||||
import { Product } from '@/lib/product';
|
||||
import { Product, pricePerUnitDisplay, productPriceFor } from '@/lib/product';
|
||||
|
||||
import initialProducts from '@/__fixtures__/initialProducts';
|
||||
|
||||
const mockAreaProduct = initialProducts.find(p => 'w' in p.dimensions ) as Product
|
||||
const mockLengthProduct = initialProducts.find(p => (!('w' in p.dimensions)) ) as Product
|
||||
|
||||
describe('ProductCalculatorSelector', () => {
|
||||
|
||||
const mockAreaProduct = new Product(
|
||||
100,
|
||||
{ l: 4, w: 8, u: "ft" },
|
||||
{"name": "area product"},
|
||||
);
|
||||
const mockLengthProduct = new Product(
|
||||
100,
|
||||
{ l: 4, u: "ft" },
|
||||
{"name": "length product"},
|
||||
);
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderWithProviders(
|
||||
(<ProductCalculatorSelector />),
|
||||
{
|
||||
products: [
|
||||
mockAreaProduct.asObject,
|
||||
mockLengthProduct.asObject,
|
||||
mockAreaProduct,
|
||||
mockLengthProduct,
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(screen.getByText('Please select a product')).toBeTruthy();
|
||||
const label = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`;
|
||||
const label = `${mockAreaProduct.attributes?.name} (${pricePerUnitDisplay(mockAreaProduct)})`;
|
||||
expect(screen.getByText(label)).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -38,15 +31,14 @@ describe('ProductCalculatorSelector', () => {
|
||||
(<ProductCalculatorSelector />),
|
||||
{
|
||||
products: [
|
||||
mockLengthProduct.asObject,
|
||||
mockAreaProduct.asObject,
|
||||
mockLengthProduct,
|
||||
mockAreaProduct,
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
expect(screen.getByText('Please select a product')).toBeTruthy();
|
||||
const areaLabel = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`;
|
||||
const lengthLabel = `${mockLengthProduct.attributes.name} (${mockLengthProduct.pricePerUnitDisplay})`;
|
||||
const areaLabel = `${mockAreaProduct.attributes?.name} (${pricePerUnitDisplay(mockAreaProduct)})`;
|
||||
|
||||
fireEvent.press(screen.getByText(areaLabel));
|
||||
const lengthInput = screen.getByLabelText("enter length");
|
||||
@ -63,10 +55,10 @@ describe('ProductCalculatorSelector', () => {
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
const price = mockAreaProduct.priceFor({l: 2, w: 4, u: "ft"});
|
||||
const price = productPriceFor(mockAreaProduct, {l: 2, w: 4, u: "ft"})
|
||||
const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,});
|
||||
const element = screen.getByLabelText("calculated price");
|
||||
const {getByText} = within(element);
|
||||
expect(getByText(sPrice)).toBeTruthy();
|
||||
expect(getByText(/\$.*0.*\.10/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { renderWithProviders } from '@/lib/rendering';
|
||||
import { Product } from '@/lib/product';
|
||||
import { Product, pricePerUnitDisplay } from '@/lib/product';
|
||||
import ProductList from '@/components/ProductList';
|
||||
import initialProducts from '@/__fixtures__/initialProducts';
|
||||
import { screen } from '@testing-library/react-native';
|
||||
@ -15,13 +15,24 @@ describe('ProductList', () => {
|
||||
});
|
||||
|
||||
it('renders products correctly', () => {
|
||||
const mockProduct = initialProducts[0];
|
||||
const mockProduct = initialProducts[0] as Product;
|
||||
const label = `${mockProduct.attributes?.name} (${pricePerUnitDisplay(mockProduct)})`;
|
||||
|
||||
const { getByText } = renderWithProviders(<ProductList />, {
|
||||
products: [mockProduct],
|
||||
});
|
||||
|
||||
expect(getByText(label)).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(getByText(mockProduct.attributes.name)).toBeTruthy();
|
||||
expect(getByText(`$${mockProduct.pricePerUnit}`)).toBeTruthy();
|
||||
it('renders only area_rug products', () => {
|
||||
const areaRug = initialProducts.find(p => p.type == "area_rug") as Product;
|
||||
const label = `${areaRug?.attributes?.name} (${pricePerUnitDisplay(areaRug)})`;
|
||||
|
||||
renderWithProviders(<ProductList productType='area_rug' />, {
|
||||
products: initialProducts,
|
||||
});
|
||||
|
||||
expect(screen.getByText(label)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react-native';
|
||||
import UnitChooser from "../UnitChooser";
|
||||
import { Length } from 'safe-units';
|
||||
import { Length } from 'convert';
|
||||
|
||||
describe('UnitChooser', () => {
|
||||
const mockOnChoicePressed = jest.fn();
|
||||
|
Reference in New Issue
Block a user