From dbba2620441b6c3a59e17353410299cdadbe6655 Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 31 Jul 2024 10:01:45 -0700 Subject: [PATCH] the type construct of components display. TODO: add area rug calculator. --- __fixtures__/initialProducts.ts | 125 ++++- app/(tabs)/_layout.tsx | 4 +- components/ProductAttributeEditor.tsx | 129 ++--- components/ProductCalculatorSelector.tsx | 8 +- components/ProductEditor.tsx | 2 +- components/ProductEditorItem.tsx | 449 ++++++++++-------- components/ProductList.tsx | 29 +- components/ProductTile.tsx | 6 +- .../__tests__/ProductAttributeEditor-test.tsx | 24 +- components/__tests__/ProductEditor-test.tsx | 92 ++-- .../__tests__/ProductEditorItem-test.tsx | 20 +- components/__tests__/ProductList-test.tsx | 27 ++ features/product/productSlice.ts | 6 +- lib/__tests__/product-test.ts | 8 - lib/dimensions.ts | 1 - lib/product.ts | 10 +- 16 files changed, 560 insertions(+), 380 deletions(-) create mode 100644 components/__tests__/ProductList-test.tsx diff --git a/__fixtures__/initialProducts.ts b/__fixtures__/initialProducts.ts index 058209b..947663a 100644 --- a/__fixtures__/initialProducts.ts +++ b/__fixtures__/initialProducts.ts @@ -1,21 +1,106 @@ -import { Product } from "@/lib/product"; +import { Product } from "@/lib/product" +import uuid from "react-native-uuid" -export const products = [ - // Sheet goods - new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/4\"" }), - new Product(20, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/2\"" }), - new Product(25, {l: 4, w : 8, u: "ft"}, { name: "Plywood 3/4\"" }), - new Product(5, {l: 4, w : 8, u: "ft"}, { name: "Thin Panel Board" }), - new Product(10, {l: 4, w : 8, u: "ft"}, { name: "Sheetrock" }), - new Product(15, {l: 4, w : 8, u: "ft"}, { name: "OSB / Particle" }), - new Product(20, {l: 4, w : 8, u: "ft"}, { name: "MDF" }), - new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Pegboard" }), - new Product(5, {l: 3, w : 5, u: "ft"}, { name: "Cement" }), - // trim - new Product(1, {l: 0.50, u : "ft"}, { name: "trim <= 3 inches" }), - new Product(1, {l: 0.75, u : "ft"}, { name: "trim > 3 inches" }), - // siding - new Product(1, {l: 1, u: "ft"}, {name: "house siding"}), - new Product(1, {l: 1, u: "ft"}, {name: "metal / shelf bars"}), - new Product(0.5, {l: 1, u: "ft"}, {name: "gutter spouts"}), -]; \ No newline at end of file +export default [ + // Sheet goods + { + id: uuid.v4().valueOf(), + pricePerUnit: 15, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: 'Plywood 1/4"' }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 20, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: 'Plywood 1/2"' }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 25, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: 'Plywood 3/4"' }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 5, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: "Thin Panel Board" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 10, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: "Sheetrock" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 15, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: "OSB / Particle" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 20, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: "MDF" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 15, + dimensions: { l: 4, w: 8, u: "ft" }, + type: "lumber", + attributes: { name: "Pegboard" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 5, + dimensions: { l: 3, w: 5, u: "ft" }, + type: "lumber", + attributes: { name: "Cement" }, + }, + // trim + { + id: uuid.v4().valueOf(), + pricePerUnit: 1, + dimensions: { l: 0.5, u: "ft" }, + type: "lumber", + attributes: { name: "trim <=3 inches" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 1, + dimensions: { l: 0.75, u: "ft" }, + type: "lumber", + attributes: { name: "trim > 3 inches" }, + }, + // siding + { + id: uuid.v4().valueOf(), + pricePerUnit: 1, + dimensions: { l: 1, u: "ft" }, + type: "lumber", + attributes: { name: "house siding" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 1, + dimensions: { l: 1, u: "ft" }, + type: "lumber", + attributes: { name: "metal / shelf bars" }, + }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 0.5, + dimensions: { l: 1, u: "ft" }, + type: "lumber", + attributes: { name: "gutter spouts" }, + }, +] as Array; \ No newline at end of file diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 8db0aa3..8ac6a2c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -4,13 +4,13 @@ import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { TabBarIcon } from '@/components/navigation/TabBarIcon'; import { Provider } from 'react-redux'; -import { products as fixtures } from "@/__fixtures__/initialProducts" +import fixtures from "@/__fixtures__/initialProducts" import { setupStore } from '../store'; export default function TabLayout() { const colorScheme = useColorScheme(); const store = setupStore({ - products: fixtures.map(p => p.asObject), + products: fixtures, units: "ft", }); return ( diff --git a/components/ProductAttributeEditor.tsx b/components/ProductAttributeEditor.tsx index f555a1e..272c6f4 100644 --- a/components/ProductAttributeEditor.tsx +++ b/components/ProductAttributeEditor.tsx @@ -1,68 +1,87 @@ -import { Product } from "@/lib/product"; +import { product_type_t } from "@/lib/dimensions"; +import { PRODUCT_TYPES, Product } from "@/lib/product"; import Ionicons from "@expo/vector-icons/Ionicons"; -import React from "react"; -import { useState } from "react"; -import { StyleSheet, Text, TextInput, TouchableHighlight, View } from "react-native"; +import { StyleSheet, TextInput, TouchableHighlight, View } from "react-native"; +import SelectDropdown from "react-native-select-dropdown"; export type ProductAttributeChangeFunc = (key: string, newValue: string) => any; export type ProductAttributeDeleteFunc = (key: string) => any; -export type ChangeAttributeFunction = (oldKey : string, newKey : string) => any; +export type ChangeAttributeFunction = (oldKey: string, newKey: string) => any; +export type ProductTypeChangeFunc = ( + key: string, + newProductType: product_type_t +) => any; export type ProductAttributeProps = { - attributeKey: string, - attributeValue: string, - onChangeAttributeKey?: ChangeAttributeFunction, - onChangeAttribute?: ProductAttributeChangeFunc, - onDelete?: ProductAttributeChangeFunc, + attributeKey: string; + attributeValue: string; + onProductTypeChange?: ProductTypeChangeFunc; + onChangeAttributeKey?: ChangeAttributeFunction; + onChangeAttribute?: ProductAttributeChangeFunc; + onDelete?: ProductAttributeChangeFunc; }; -export const ProductAttributeEditor = ({ attributeKey, attributeValue, onDelete, onChangeAttributeKey, onChangeAttribute }: ProductAttributeProps) => { +const select_product_type_choices = PRODUCT_TYPES.map((p) => [p, p]); - const doChangeKey = (e: any) => { - onChangeAttributeKey && onChangeAttributeKey(attributeKey, e); - } +export const ProductAttributeEditor = ({ + attributeKey, + attributeValue, + onDelete, + onChangeAttributeKey, + onChangeAttribute, +}: ProductAttributeProps) => { + const doChangeKey = (e: any) => { + onChangeAttributeKey && onChangeAttributeKey(attributeKey, e); + }; - const doChangeValue = (e: any) => { - onChangeAttribute && onChangeAttribute(attributeKey, e); - } + const doChangeValue = (e: any) => { + onChangeAttribute && onChangeAttribute(attributeKey, e); + }; - return ( - - - - - onDelete && onDelete(attributeKey, attributeValue)} - aria-label="Delete Attribute" - style={{ backgroundColor: "darkred", borderRadius: 5, margin: 5, padding: 5, }}> - - - - - ) -} + return ( + + + + + onDelete && onDelete(attributeKey, attributeValue)} + aria-label="Delete Attribute" + style={{ + backgroundColor: "darkred", + borderRadius: 5, + margin: 5, + padding: 5, + }} + > + + + + + ); +}; const styles = StyleSheet.create({ - productAttributeRow: { - flexDirection: "row", - }, - key: { - flex: 1, - }, - value: { - flex: 1, - borderWidth: 1, - borderColor: "grey", - borderStyle: "solid", - padding: 10 - } -}); \ No newline at end of file + productAttributeRow: { + flexDirection: "row", + }, + key: { + flex: 1, + }, + value: { + flex: 1, + borderWidth: 1, + borderColor: "grey", + borderStyle: "solid", + padding: 10, + }, +}); diff --git a/components/ProductCalculatorSelector.tsx b/components/ProductCalculatorSelector.tsx index c6153c1..a84901b 100644 --- a/components/ProductCalculatorSelector.tsx +++ b/components/ProductCalculatorSelector.tsx @@ -1,5 +1,5 @@ -import { Product } from '@/lib/product'; -import { dimensions_t } from "@/lib/dimensions_t"; +import { Product, productPriceFor } from '@/lib/product'; +import { dimensions_t } from "@/lib/dimensions"; import { useState, useEffect } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -23,7 +23,7 @@ export default function ProductCalculatorSelector() { const iv = setInterval(function () { if (!(activeProduct && measurement)) return; setPrice( - activeProduct.priceFor(measurement, percentDamage) + productPriceFor(activeProduct, measurement, percentDamage) ) }, 50); return function () { @@ -34,7 +34,7 @@ export default function ProductCalculatorSelector() { function onMeasurementSet(dimensions: dimensions_t) { setMeasurement(dimensions); activeProduct && setPrice( - activeProduct.priceFor(measurement, percentDamage) + productPriceFor(activeProduct, measurement, percentDamage) ) } diff --git a/components/ProductEditor.tsx b/components/ProductEditor.tsx index cf97ebe..f0f157e 100644 --- a/components/ProductEditor.tsx +++ b/components/ProductEditor.tsx @@ -1,9 +1,9 @@ import { useAppDispatch, useAppSelector } from "@/app/store" import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice" import { Id, Product } from "@/lib/product"; -import { dimensions_t } from "@/lib/dimensions_t"; import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native"; import { ProductEditorItem } from "./ProductEditorItem"; +import { dimensions_t } from "@/lib/dimensions"; export const ProductEditor = ({}) => { const products = useAppSelector(selectProducts) as Product []; diff --git a/components/ProductEditorItem.tsx b/components/ProductEditorItem.tsx index f73bff2..7eef933 100644 --- a/components/ProductEditorItem.tsx +++ b/components/ProductEditorItem.tsx @@ -1,231 +1,274 @@ -import { Id, Product } from "@/lib/product" -import { dimensions_t } from "@/lib/dimensions_t"; -import { useState } from "react" -import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native" +import { Id, Product, product_type_t } from "@/lib/product"; +import { useState } from "react"; +import { + Button, + FlatList, + StyleSheet, + Text, + TextInput, + Touchable, + TouchableHighlight, + View, +} from "react-native"; import { ProductAttributeEditor } from "./ProductAttributeEditor"; -import { Dropdown } from 'react-native-element-dropdown'; +import { Dropdown } from "react-native-element-dropdown"; import { Ionicons } from "@expo/vector-icons"; import { Length } from "convert"; +import { dimensions_t } from "@/lib/dimensions"; export type ProductAddedFunc = () => any; export type ProductDeletedFunc = (product_id: Id) => any; export type AttributeAddedFunc = (product_id: Id) => any; -export type AttributeKeyUpdatedFunc = (product_id: Id, oldKey: string, newKey: string) => any; -export type AttributeUpdatedFunc = (product_id: Id, attribute: string, value: string) => any; +export type AttributeKeyUpdatedFunc = ( + product_id: Id, + oldKey: string, + newKey: string +) => any; +export type AttributeUpdatedFunc = ( + product_id: Id, + attribute: string, + value: string +) => any; export type AttributeDeletedFunc = (product_id: Id, attribute: string) => any; export type PriceUpdatedFunc = (product_id: Id, price: number) => any; -export type DimensionUpdatedFunc = (product_id: Id, dimension: dimensions_t) => any; +export type DimensionUpdatedFunc = ( + product_id: Id, + dimension: dimensions_t +) => any; +export type ProductTypeChangedFunc = ( + product_id: Id, + product_type: product_type_t +) => any; export type ProductEditorItemProps = { - product: Product, - onProductAdded?: ProductAddedFunc, - onProductDeleted?: ProductDeletedFunc, - onAttributeAdded?: AttributeAddedFunc, - onAttributeKeyChanged?: AttributeKeyUpdatedFunc, - onAttributeUpdated?: AttributeUpdatedFunc, - onAttributeDeleted?: AttributeDeletedFunc, - onPriceUpdated?: PriceUpdatedFunc, - onDimensionsUpdated?: DimensionUpdatedFunc, -} + product: Product; + onProductAdded?: ProductAddedFunc; + onProductDeleted?: ProductDeletedFunc; + onAttributeAdded?: AttributeAddedFunc; + onAttributeKeyChanged?: AttributeKeyUpdatedFunc; + onAttributeUpdated?: AttributeUpdatedFunc; + onAttributeDeleted?: AttributeDeletedFunc; + onPriceUpdated?: PriceUpdatedFunc; + onDimensionsUpdated?: DimensionUpdatedFunc; + onProductTypeChanged?: ProductTypeChangedFunc; +}; export const ProductEditorItem = (props: ProductEditorItemProps) => { + const [showAttributes, setShowAttributes] = useState(false); + const product = props.product; - const [showAttributes, setShowAttributes] = useState(false); - const product = props.product; + function onProductTypeChange(id: Id, newProductType: product_type_t) { + props.onProductTypeChanged && + props.onProductTypeChanged(product.id as Id, newProductType); + } - function onAttributeChanged(key: string, newValue: string) { - props.onAttributeUpdated && props.onAttributeUpdated(product.id, key, newValue); - } + function onAttributeChanged(key: string, newValue: string) { + props.onAttributeUpdated && + props.onAttributeUpdated(product.id as Id, key, newValue); + } - function onAttributeKeyChanged(oldKey: string, newKey: string) { - props.onAttributeKeyChanged && props.onAttributeKeyChanged(product.id, oldKey, newKey); - } + function onAttributeKeyChanged(oldKey: string, newKey: string) { + props.onAttributeKeyChanged && + props.onAttributeKeyChanged(product.id as Id, oldKey, newKey); + } - function onAttributeDelete(key: string) { - props.onAttributeDeleted && props.onAttributeDeleted(product.id, key); - } + function onAttributeDelete(key: string) { + props.onAttributeDeleted && props.onAttributeDeleted(product.id as Id, key); + } - function onPricePerUnitChange(pricePerUnit: string) { - props.onPriceUpdated && props.onPriceUpdated(product.id, parseFloat(pricePerUnit) || parseInt(pricePerUnit)); - } + function onPricePerUnitChange(pricePerUnit: string) { + props.onPriceUpdated && + props.onPriceUpdated( + product.id as Id, + parseFloat(pricePerUnit) || parseInt(pricePerUnit) + ); + } - function onUnitsChanged(newUnits: Length) { - props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, { - ...(product.dimensions as dimensions_t), - u: newUnits, - }) - } + function onUnitsChanged(newUnits: Length) { + props.onDimensionsUpdated && + props.onDimensionsUpdated(product.id as Id, { + ...(product.dimensions as dimensions_t), + u: newUnits, + }); + } - function onChangeLength(len: string) { - const l = parseFloat(len) || parseInt(len); - props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, { - ...(product.dimensions as dimensions_t), - l, - }) - } + function onChangeLength(len: string) { + const l = parseFloat(len) || parseInt(len); + props.onDimensionsUpdated && + props.onDimensionsUpdated(product.id as Id, { + ...(product.dimensions as dimensions_t), + l, + }); + } - function onChangeWidth(width: string) { - const w = width.length == 0 ? null : parseFloat(width) || parseInt(width); - props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, { - ...(product.dimensions as dimensions_t), - ...(w ? {w} : {}), - }) - } + function onChangeWidth(width: string) { + const w = width.length == 0 ? null : parseFloat(width) || parseInt(width); + props.onDimensionsUpdated && + props.onDimensionsUpdated(product.id as Id, { + ...(product.dimensions as dimensions_t), + ...(w ? { w } : {}), + }); + } - function onDeleteProduct() { - props.onProductDeleted && props.onProductDeleted(product.id); - } + function onDeleteProduct() { + props.onProductDeleted && props.onProductDeleted(product.id as Id); + } - const length = new String(product.dimensions.l || product.dimensions.l || "0") as string; - const width = new String(product.dimensions.w || "") as string; - const dimension = product.dimensions.u || product.dimensions.u || "foot"; + const length = new String( + product.dimensions.l || product.dimensions.l || "0" + ) as string; + const width = new String(product.dimensions.w || "") as string; + const dimension = product.dimensions.u || product.dimensions.u || "foot"; - return ( - - - setShowAttributes(!showAttributes)} - aria-label="Product Item" - style={styles.productItemName} - > - {product.attributes.name || `Product ${product.id}`} - - onDeleteProduct()} - aria-label="delete product" - style={styles.deleteProductHighlight} - > - - - - {showAttributes && - ( - - - $ - - per - onUnitsChanged(item.value as Length)} - /> - - x - - -