the type construct of components display. TODO: add area rug calculator.

This commit is contained in:
Jordan 2024-07-31 10:01:45 -07:00
parent 23d957824b
commit dbba262044
16 changed files with 560 additions and 380 deletions

View File

@ -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"}),
];
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<Product>;

View File

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

View File

@ -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 (
<View>
<View style={styles.productAttributeRow}>
<TextInput
defaultValue={attributeKey}
onChangeText={doChangeKey}
style={styles.value}
aria-label="Edit Key"
/>
<TextInput
defaultValue={attributeValue}
onChangeText={doChangeValue}
style={styles.value}
aria-label="Edit Value" />
<TouchableHighlight
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
aria-label="Delete Attribute"
style={{ backgroundColor: "darkred", borderRadius: 5, margin: 5, padding: 5, }}>
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
</TouchableHighlight>
</View>
</View>
)
}
return (
<View>
<View style={styles.productAttributeRow}>
<TextInput
defaultValue={attributeKey}
onChangeText={doChangeKey}
style={styles.value}
aria-label="Edit Key"
/>
<TextInput
defaultValue={attributeValue}
onChangeText={doChangeValue}
style={styles.value}
aria-label="Edit Value"
/>
<TouchableHighlight
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
aria-label="Delete Attribute"
style={{
backgroundColor: "darkred",
borderRadius: 5,
margin: 5,
padding: 5,
}}
>
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
</TouchableHighlight>
</View>
</View>
);
};
const styles = StyleSheet.create({
productAttributeRow: {
flexDirection: "row",
},
key: {
flex: 1,
},
value: {
flex: 1,
borderWidth: 1,
borderColor: "grey",
borderStyle: "solid",
padding: 10
}
productAttributeRow: {
flexDirection: "row",
},
key: {
flex: 1,
},
value: {
flex: 1,
borderWidth: 1,
borderColor: "grey",
borderStyle: "solid",
padding: 10,
},
});

View File

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

View File

@ -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 [];

View File

@ -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 (
<View>
<View style={styles.productListHeader}>
<TouchableHighlight
onPress={() => setShowAttributes(!showAttributes)}
aria-label="Product Item"
style={styles.productItemName}
>
<Text style={styles.productNameText}>{product.attributes.name || `Product ${product.id}`}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={() => onDeleteProduct()}
aria-label="delete product"
style={styles.deleteProductHighlight}
>
<Ionicons
style={styles.deleteProductButton}
name="trash-outline"
/>
</TouchableHighlight>
</View>
{showAttributes &&
(
<View style={styles.detailsWrapper}>
<View style={styles.priceSpecWrapper}>
<Text style={styles.priceLabel}>$</Text>
<TextInput inputMode="decimal"
defaultValue={new String(product.pricePerUnit) as string}
aria-label="price per unit"
onChangeText={onPricePerUnitChange}
style={styles.priceInput}
/>
<Text style={styles.per}>per</Text>
<Dropdown
data={[
{label: "feet", value: "ft"},
{label: "inches", value: "in"},
]}
style={styles.unitsSelect}
mode="modal"
labelField="label"
valueField="value"
value={product.dimensions.u || "ft"}
onChange={(item) => onUnitsChanged(item.value as Length)}
/>
<TextInput
inputMode="decimal"
defaultValue={length}
onChangeText={onChangeLength}
style={styles.lengthInput}
aria-label="length"
/>
<Text style={{flex: 1,}}>x</Text>
<TextInput
inputMode="decimal"
defaultValue={width}
onChangeText={onChangeWidth}
style={styles.widthInput}
aria-label="width"
/>
</View>
<Button title="+ Add Attribute" onPress={() => props.onAttributeAdded && props.onAttributeAdded(product.id)} />
<FlatList
style={styles.productAttributesList}
data={Object.entries(product.attributes)}
renderItem={({ item }) => (
<ProductAttributeEditor
attributeKey={item[0] || "some key"}
attributeValue={item[1]}
onChangeAttributeKey={onAttributeKeyChanged}
onChangeAttribute={onAttributeChanged}
onDelete={onAttributeDelete}
/>
)}
keyExtractor={(item, i) => `${product.id}-${i}`}
/>
</View>
)
return (
<View>
<View style={styles.productListHeader}>
<TouchableHighlight
onPress={() => setShowAttributes(!showAttributes)}
aria-label="Product Item"
style={styles.productItemName}
>
{product.attributes && (
<Text style={styles.productNameText}>
{product.attributes.name || `Product ${product.id}`}
</Text>
)}
</TouchableHighlight>
<TouchableHighlight
onPress={() => onDeleteProduct()}
aria-label="delete product"
style={styles.deleteProductHighlight}
>
<Ionicons style={styles.deleteProductButton} name="trash-outline" />
</TouchableHighlight>
</View>
{showAttributes && (
<View style={styles.detailsWrapper}>
<View style={styles.priceSpecWrapper}>
<Text style={styles.priceLabel}>$</Text>
<TextInput
inputMode="decimal"
defaultValue={new String(product.pricePerUnit).valueOf()}
aria-label="price per unit"
onChangeText={onPricePerUnitChange}
style={styles.priceInput}
/>
<Text style={styles.per}>per</Text>
<Dropdown
data={[
{ label: "feet", value: "ft" },
{ label: "inches", value: "in" },
]}
style={styles.unitsSelect}
mode="modal"
labelField="label"
valueField="value"
value={product.dimensions.u || "ft"}
onChange={(item) => onUnitsChanged(item.value as Length)}
/>
<TextInput
inputMode="decimal"
defaultValue={length}
onChangeText={onChangeLength}
style={styles.lengthInput}
aria-label="length"
/>
<Text style={{ flex: 1 }}>x</Text>
<TextInput
inputMode="decimal"
defaultValue={width}
onChangeText={onChangeWidth}
style={styles.widthInput}
aria-label="width"
/>
</View>
<Button
title="+ Add Attribute"
onPress={() =>
props.onAttributeAdded && props.onAttributeAdded(product.id as Id)
}
/>
{product.attributes && (
<FlatList
style={styles.productAttributesList}
data={Object.entries(product.attributes)}
renderItem={({ item }) => (
<ProductAttributeEditor
onProductTypeChange={onProductTypeChange}
attributeKey={item[0] || "some key"}
attributeValue={item[1]}
onChangeAttributeKey={onAttributeKeyChanged}
onChangeAttribute={onAttributeChanged}
onDelete={onAttributeDelete}
/>
)}
keyExtractor={(item, i) => `${product.id}-${i}`}
/>
)}
</View>
)
}
)}
</View>
);
};
const styles = StyleSheet.create({
deleteProductHighlight: {
padding: 5,
borderWidth: 1,
},
deleteProductButton: {
fontSize: 20,
},
detailsWrapper: {
},
priceSpecWrapper: {
flexDirection: "row",
},
priceLabel: {
},
priceInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
per: {
padding: 5,
},
unitsLabel: {
},
unitsSelect: {
flex: 1,
padding: 5,
},
lengthInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
widthInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
productListHeader: {
flexDirection: "row",
padding: 5,
},
productNameText: {
paddingLeft: 10,
paddingRight: 10,
},
productItemName: {
flex: 1,
backgroundColor: "lightgrey",
padding: 4,
margin: 4,
},
productAttributesList: {
margin: 10,
padding: 10,
borderWidth: 1,
borderStyle: "solid",
borderColor: "black",
},
})
deleteProductHighlight: {
padding: 5,
borderWidth: 1,
},
deleteProductButton: {
fontSize: 20,
},
detailsWrapper: {},
priceSpecWrapper: {
flexDirection: "row",
},
priceLabel: {},
priceInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
per: {
padding: 5,
},
unitsLabel: {},
unitsSelect: {
flex: 1,
padding: 5,
},
lengthInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
widthInput: {
flex: 1,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
productListHeader: {
flexDirection: "row",
padding: 5,
},
productNameText: {
paddingLeft: 10,
paddingRight: 10,
},
productItemName: {
flex: 1,
backgroundColor: "lightgrey",
padding: 4,
margin: 4,
},
productAttributesList: {
margin: 10,
padding: 10,
borderWidth: 1,
borderStyle: "solid",
borderColor: "black",
},
});

View File

@ -1,19 +1,22 @@
import { FlatList, ScrollView, StyleSheet, Text, TouchableHighlight } from "react-native";
import { ScrollView, StyleSheet } from "react-native";
import { ProductTile } from "./ProductTile";
import { Id, Product } from "@/lib/product";
import { Key, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Product } 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;
}
export default function ProductList({ onProductSelected }: ProductSelectionProps) {
};
export default function ProductList({
onProductSelected,
}: ProductSelectionProps) {
const [activeProduct, setActiveProduct] = useState(null as null | Product);
const products = useAppSelector(selectProducts).filter(p => (!!p.dimensions));
const products = useAppSelector(selectProducts).filter(p => !!p).filter((p) => {
console.dir(p);
return !!p.dimensions;
});
function doOnProductSelected(product: Product) {
setActiveProduct(product);
@ -21,8 +24,8 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
}
return (
<ScrollView scrollToOverflowEnabled={true}>
{products.map(product => {
<ScrollView scrollToOverflowEnabled={true} aria-label="product list">
{products.map((product) => {
return (
<ProductTile
product={product}
@ -33,14 +36,12 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
);
})}
</ScrollView>
)
);
}
const styles = StyleSheet.create({
productSelectorFlatList: {
padding: 10,
margin: 10,
},
})
});

View File

@ -1,4 +1,4 @@
import { Product } from "@/lib/product"
import { Product, priceDisplay, pricePerUnitDisplay } from "@/lib/product"
import { ImageBackground, StyleProp, StyleSheet, Text, TouchableHighlight, View, ViewStyle } from "react-native";
import { AnimatedStyle } from "react-native-reanimated";
@ -23,12 +23,14 @@ const FALLBACK_IMAGE = "";
export function ProductTile ({product, onProductSelected, isActive} : ProductTileProps) {
const k = isActive ? "active" : "default";
const priceDisplay = pricePerUnitDisplay(product);
return (
<TouchableHighlight
style={styles[k].highlight}
onPress={() => onProductSelected && onProductSelected(product)}>
<Text style={styles[k].text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
<Text style={styles[k].text}>{product.attributes?.name || `Product ${product.id}`} ({priceDisplay})</Text>
</TouchableHighlight>
);
}

View File

@ -1,25 +1,16 @@
import { Product } from "@/lib/product"
import { LumberProduct, Product } from "@/lib/product"
import {ProductAttributeEditor} from "../ProductAttributeEditor"
import { area } from "enheter"
import { fireEvent, render, screen } from '@testing-library/react-native';
import React from "react";
import { emitTypingEvents } from "@testing-library/react-native/build/user-event/type/type";
describe("Product editor tests", () => {
const productName = "Fun Product";
it("Product attributes can be deleted", async () => {
const product = new Product(
100,
{l: 100, u: "foot"},
{"name" : productName}
);
const onChange = jest.fn();
const onDelete = jest.fn();
render(
<ProductAttributeEditor
attributeKey="name"
attributeValue="product"
product={product}
onChangeAttribute={onChange}
onDelete={onDelete}
/>);
@ -28,11 +19,14 @@ describe("Product editor tests", () => {
expect(onDelete).toHaveBeenCalled();
});
it("Product attributes can be modified", async () => {
const product = new Product(
100,
{l: 100, u: "foot"},
{"name" : productName}
);
const product : Product = {
pricePerUnit: 10,
dimensions: {
l: 40,
u: "ft",
},
type: "lumber",
}
const onChange = jest.fn();
const onDelete = jest.fn();
const onKeyChange = jest.fn();

View File

@ -2,50 +2,56 @@ import { renderWithProviders } from "@/lib/rendering";
import { ProductEditor } from "@/components/ProductEditor";
import { act, fireEvent, screen } from "@testing-library/react-native";
import { selectProducts } from "@/features/product/productSlice";
import { Product } from "@/lib/product";
import { LumberProduct, Product } from "@/lib/product";
describe("ProductEditor", () => {
const productName = "Flooring"
const mockProduct = new Product(
25,
{ l: 4, w: 8, u: "foot" },
{ name: productName },
)
it("renders correctly", async () => {
const { store } = renderWithProviders(<ProductEditor />, {
products: [
mockProduct.asObject,
],
});
const state1 = store.getState();
let products = selectProducts(state1);
expect(products).toHaveLength(1);
// Check if the product names are rendered
expect(screen.getByText(products[0].attributes.name as string)).toBeTruthy();
// Start to edit a product
fireEvent.press(screen.getByText(productName));
// Change properties of the product to make sure it's updated in the store
act(() => {
fireEvent.changeText(screen.getByLabelText("length"), "16");
})
products = selectProducts(store.getState());
expect(products[0].dimensions.l).toBe(16);
act(() => {
fireEvent.changeText(screen.getByLabelText("width"), "32");
})
products = selectProducts(store.getState());
expect(products[0].dimensions.w).toBe(32);
fireEvent.press(screen.getByLabelText("delete product"));
products = selectProducts(store.getState());
expect(products.length).toBe(0);
const productName = "Flooring";
const mockProduct: LumberProduct = {
attributes: {
name: productName,
},
pricePerUnit: 10,
dimensions: {
l: 40,
u: "ft",
},
type: "lumber",
};
it("renders correctly", async () => {
const { store } = renderWithProviders(<ProductEditor />, {
products: [mockProduct],
});
const state1 = store.getState();
let products = selectProducts(state1);
expect(products).toHaveLength(1);
// Check if the product names are rendered
expect(
screen.getByText(products[0].attributes.name as string)
).toBeTruthy();
// Start to edit a product
fireEvent.press(screen.getByText(productName));
// Change properties of the product to make sure it's updated in the store
act(() => {
fireEvent.changeText(screen.getByLabelText("length"), "16");
});
products = selectProducts(store.getState());
expect(products[0].dimensions.l).toBe(16);
act(() => {
fireEvent.changeText(screen.getByLabelText("width"), "32");
});
products = selectProducts(store.getState());
expect(products[0].dimensions.w).toBe(32);
fireEvent.press(screen.getByLabelText("delete product"));
products = selectProducts(store.getState());
expect(products.length).toBe(0);
});
});

View File

@ -4,14 +4,22 @@ import { ProductEditorItem } from '../ProductEditorItem';
import { Product } from '@/lib/product';
import { area } from 'enheter';
import { renderWithProviders } from '@/lib/rendering';
import { area_t } from '@/lib/dimensions';
describe('ProductEditorItem', () => {
const productName = "Product 1";
const mockProduct = new Product(
25,
{l: 4, u: 'feet'},
{"name": productName},
)
const mockProduct : Product = {
type: "area_rug",
dimensions: {
l: 1,
w: 1,
u: "feet",
},
pricePerUnit: 0.75,
attributes: {
name: productName,
}
}
const onAttributeAdded = jest.fn();
const mockOnProductDeleted = jest.fn();
@ -56,7 +64,7 @@ describe('ProductEditorItem', () => {
}
);
fireEvent.press(screen.getByText("Product 1"));
expect(screen.getByLabelText("units")).toBeTruthy();
// expect(screen.getByLabelText("Units")).toBeTruthy();
expect(screen.getByLabelText("Edit Key")).toBeTruthy();
expect(screen.getAllByLabelText("Edit Value").length).toEqual(1);

View File

@ -0,0 +1,27 @@
import React from 'react';
import { renderWithProviders } from '@/lib/rendering';
import { Product } from '@/lib/product';
import ProductList from '@/components/ProductList';
import initialProducts from '@/__fixtures__/initialProducts';
import { screen } from '@testing-library/react-native';
describe('ProductList', () => {
it('renders without crashing', () => {
const { getByTestId } = renderWithProviders(<ProductList />, {
products: initialProducts,
});
expect(screen.getByLabelText('product list')).toBeTruthy();
});
it('renders products correctly', () => {
const mockProduct = initialProducts[0];
const { getByText } = renderWithProviders(<ProductList />, {
products: [mockProduct],
});
expect(getByText(mockProduct.attributes.name)).toBeTruthy();
expect(getByText(`$${mockProduct.pricePerUnit}`)).toBeTruthy();
});
});

View File

@ -151,7 +151,7 @@ const productsState = createSlice({
}
});
export const selectProductsDatas = (state: RootState) => {
export const selectProducts = (state: RootState) => {
return state.products;
}
@ -159,10 +159,6 @@ export const selectUnits = (state : RootState) => {
return (state.units || "ft") as Length;
}
export const selectProducts = createSelector([selectProductsDatas], productsData => {
return productsData.map(d => Product.fromObject(d));
})
export const selectProductIds = createSelector([selectProducts], products => {
return products.map(p => p.id);
})

View File

@ -1,4 +1,3 @@
import { length } from "enheter";
import { Product } from "../product";
describe("Product tests", () => {
@ -20,11 +19,4 @@ describe("Product tests", () => {
const comparison = standard.priceFor({l : 24, u: "inch"});
expect(comparison).toBeCloseTo(20, 4);
});
it("Can convert to/from object", () => {
const standard = new Product(10, {l: 1, u : "feet"});
const obj = standard.asObject;
const back = Product.fromObject(obj);
expect(back).toEqual(standard);
})
});

View File

@ -11,7 +11,6 @@ export type area_t = length_t & {
export type dimensions_t = area_t | length_t;
export type product_type_t = "area" | "length";
export const isArea = (d: dimensions_t) => ("width" in d);
export const isLength = (d: dimensions_t) => (!("width" in d));

View File

@ -1,6 +1,7 @@
import uuid from "react-native-uuid";
import { dimensions_t, area_t } from "./dimensions";
import { matchDimensions } from "./dimensions";
import { Area, Length, Unit } from "convert";
export type Id = string;
@ -15,11 +16,18 @@ export type ProductAttributes = {
currency?: Currency,
[index:string]: any,
}
export function dimensionArea(d: dimensions_t) {
return "w" in d ? d.w * d.l : 0;
}
export type product_type_t = "lumber" | "area_rug"
export const PRODUCT_TYPES = [
"lumber",
"area_rug",
] as const;
export type product_type_t = typeof PRODUCT_TYPES[number];
export type Product = {
id?: Id;