add area carpet fixture. Add carpet roll calculator test. refactor carpet roll as own component. add icons.

This commit is contained in:
Jordan
2024-08-10 10:06:25 -07:00
parent dbba262044
commit a463189052
19 changed files with 471 additions and 65 deletions

View File

@ -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>

View 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;

View File

@ -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);

View File

@ -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>

View File

@ -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}
/>

View 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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();