add svg icons. running into scroll issue. will upgrade packages.

This commit is contained in:
Jordan
2024-08-15 14:07:19 -07:00
parent a463189052
commit dc7f4b25a9
38 changed files with 3272 additions and 2097 deletions

View File

@ -1,8 +1,11 @@
import { MeasurementInput } from "./MeasurementInput";
import { area_t, dimensions_t } from "@/lib/dimensions_t";
import { area_t, dimensions_t } from "@/lib/dimensions";
import { Length } from "convert";
import { useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import MeasurementUnitInput from "./MeasurementUnitInput";
import { useAppDispatch, useAppSelector } from "@/app/store";
import { selectPlywoodCalc } from "@/features/product/productSlice";
export type AreaInputProps = {
onMeasurementSet?: (area : dimensions_t) => any,
@ -14,48 +17,55 @@ export type AreaInputProps = {
export function AreaInput({onMeasurementSet, lengthLabel, widthLabel, defaultValue, units} : AreaInputProps) {
defaultValue = defaultValue || {l: 0, w: 0, u: "ft"}
units = units || "ft"
const [area, setArea] = useState(defaultValue)
function doOnLengthSet(measurement : dimensions_t) {
setArea({
...area,
l: measurement.l
});
onMeasurementSet && onMeasurementSet({
...area,
l: measurement.l
});
function doOnLengthSet(l: number) {
const a : area_t = { ...area, l };
setArea(a);
onMeasurementSet && onMeasurementSet(a);
}
function doOnWidthSet(measurement : dimensions_t) {
setArea({
...area,
w: measurement.l
});
onMeasurementSet && onMeasurementSet({
...area,
w: measurement.l
});
function doOnLengthUnitSet(u: Length) {
const a : area_t = { ...area, u };
setArea(a);
onMeasurementSet && onMeasurementSet(a);
}
function doOnWidthSet(l: number) {
const a : area_t = { ...area, l };
setArea(a);
onMeasurementSet && onMeasurementSet(a);
}
function doOnWidthUnitSet(u: Length) {
const a : area_t = { ...area, u };
setArea(a);
onMeasurementSet && onMeasurementSet(a);
}
return (
<View style={styles.areaInputWrapper}>
<MeasurementInput
defaultValue={{l: area.l, u: area.u}}
<MeasurementUnitInput
label="Length"
defaultValue={0}
defaultUnit={units}
onValueSet={doOnLengthSet}
label={lengthLabel}
units={units}
/>
onUnitSet={doOnLengthUnitSet}
aria-label="length"
/>
<Text style={{fontSize: 30,}} > x </Text>
<MeasurementInput
defaultValue={{l: area.w, u: area.u}}
<MeasurementUnitInput
label="Width"
defaultValue={0}
defaultUnit={units}
onValueSet={doOnWidthSet}
label={widthLabel}
units={units}
/>
onUnitSet={doOnWidthUnitSet}
aria-label="width"
/>
</View>
)
}

View File

@ -32,17 +32,23 @@ export const AreaRugTag = (props: AreaRugTagProps) => {
)
};
const BIG_FONT_SIZE = 30;
const styles = StyleSheet.create({
component: {
paddingVertical: 100,
flex: 1,
alignItems: "center",
},
dimensions: {
fontSize: BIG_FONT_SIZE,
},
price: {
fontSize: BIG_FONT_SIZE,
},
date: {
fontSize: BIG_FONT_SIZE,
},
tagColor: {
fontSize: BIG_FONT_SIZE,
},
})

View File

@ -1,19 +1,17 @@
import React, { useEffect, useState } from "react";
import { View, Text, TextInput, Button, StyleSheet } from "react-native";
import {
productPriceFor,
priceDisplay,
pricePerUnitDisplay,
Product,
} from "@/lib/product";
import { useEffect, useState } from "react";
import { View, Text, StyleSheet } from "react-native";
import { 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 convert, { Length } from "convert";
import ProductList from "@/components/ProductList";
import { HelpfulMeasurementUnitInput } from "./HelpfulMeasurementInput";
import { ScrollView } from "react-native-gesture-handler";
const DEFAULT_UNIT: Length = "ft";
const DEFAULT_DIAMETER_UNIT: Length = "in";
const DEFAULT_LENGTH_UNIT: Length = "ft";
export const CarpetRollCalculator = () => {
const products = useAppSelector(selectProducts);
@ -21,26 +19,40 @@ export const CarpetRollCalculator = () => {
const [width, setWidth] = useState(0);
const [outerDiameter, setOuterDiameter] = useState<length_t>({
l: 0,
u: DEFAULT_UNIT,
u: DEFAULT_DIAMETER_UNIT,
});
const [innerDiameter, setInnerDiameter] = useState<length_t>({
l: 0,
u: DEFAULT_UNIT,
u: DEFAULT_DIAMETER_UNIT,
});
const [numRings, setNumRings] = useState(0);
const [price, setPrice] = useState(0);
const [rugDimensions, setRugDimensions] = useState<area_t>({
u: DEFAULT_UNIT,
u: DEFAULT_LENGTH_UNIT,
w: 0,
l: 0,
});
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [units, setUnits] = useState<Length>(DEFAULT_UNIT);
const [units, setUnits] = useState<Length>(DEFAULT_LENGTH_UNIT);
useEffect(() => {
console.log(`recalculating...`);
// convert the "diameter" units to the length unit.
const outerD2Value = convert(outerDiameter.l, outerDiameter.u).to(units);
const innerD2Value = convert(innerDiameter.l, innerDiameter.u).to(units);
const innerD2 = {
l: innerD2Value,
u: units,
};
const outerD2 = {
l: outerD2Value,
u: units,
};
const l = diameterToLength(outerD2, innerD2, numRings).l;
const dimens = {
l: diameterToLength(outerDiameter, innerDiameter, numRings).l || 0.0,
l,
w: width || selectedProduct?.dimensions.l || 0.0,
u: units || selectedProduct?.dimensions.u || "ft",
};
@ -50,46 +62,65 @@ export const CarpetRollCalculator = () => {
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>
{selectedProduct ? (
<AreaRugTag dimensions={rugDimensions} product={selectedProduct} />
) : (
<Text style={styles.placeholder}>Please Select a Product</Text>
)}
</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 style={{ flex: 1, }}>
<ScrollView>
<View style={styles.inputFields}>
<View style={styles.inputFieldWrapper}>
<HelpfulMeasurementUnitInput
label="Length"
svgUri="/assets/images/icons/carpet-roll-length-raw.svg"
onUnitSet={setUnits}
onValueSet={setWidth}
defaultValue={width}
defaultUnit={units}
unitChoices={["ft", "in"]}
/>
</View>
<View style={styles.inputFieldWrapper}>
<HelpfulMeasurementUnitInput
label="Inner diameter"
svgUri="/assets/images/icons/carpet-roll-length-inner-diameter-raw.svg"
onUnitSet={(u) => setInnerDiameter({ ...innerDiameter, u })}
defaultValue={innerDiameter.l}
defaultUnit={innerDiameter.u}
unitChoices={["ft", "in"]}
onValueSet={(l) => setInnerDiameter({ ...innerDiameter, l })}
/>
</View>
<View style={styles.inputFieldWrapper}>
<HelpfulMeasurementUnitInput
label="Outer diameter"
svgUri="/assets/images/icons/carpet-roll-length-outer-diameter-raw.svg"
onUnitSet={(u) => setOuterDiameter({ ...outerDiameter, u })}
defaultValue={innerDiameter.l}
defaultUnit={innerDiameter.u}
unitChoices={["ft", "in"]}
onValueSet={(l) => setOuterDiameter({ ...outerDiameter, l })}
/>
</View>
<View style={styles.inputFieldWrapper}>
<HelpfulMeasurementUnitInput
label="Number of rings"
svgUri="/assets/images/icons/carpet-roll-length-number-of-rings-raw.svg"
defaultValue={0}
onValueSet={setNumRings}
/>
</View>
</View>
<View style={styles.container}>
<ProductList
onProductSelected={setSelectedProduct}
productType="area_rug"
/>
</View>
</ScrollView>
</View>
</View>
);
@ -97,9 +128,29 @@ export const CarpetRollCalculator = () => {
const styles = StyleSheet.create({
container: {
flexGrow: 1,
flex: 1,
justifyContent: "center",
padding: 20,
},
placeholder: {
alignContent: "center",
alignSelf: "center",
paddingTop: 50,
paddingBottom: 50,
fontSize: 30,
},
inputFieldWrapper: {
padding: 10,
},
inputFields: {},
label: {
flex: 1,
flexDirection: "row",
},
numberInput: {
flexDirection: "row",
borderStyle: "solid",
borderColor: "black",
borderWidth: 1,
},
});

View File

@ -0,0 +1,33 @@
import { StyleSheet, Text, View } from "react-native";
import { MeasurementInputProps } from "./MeasurementInput";
import MeasurementUnitInput, {
MeasurementUnitInputProps,
} from "./MeasurementUnitInput";
import { SvgUri } from "react-native-svg";
import { Length } from "convert";
export type HelpfulMeasurementUnitInputParams = MeasurementUnitInputProps & {
svgUri: string;
label: string;
unitChoices?: Length[];
};
export function HelpfulMeasurementUnitInput(
props: HelpfulMeasurementUnitInputParams
) {
return (
<View>
<SvgUri uri={props.svgUri} width="100px" height="100px" />
<Text>{props.label}</Text>
<MeasurementUnitInput
defaultUnit={props.defaultUnit || "ft"}
defaultValue={props.defaultValue || 0.0}
onUnitSet={props.onUnitSet}
onValueSet={props.onValueSet}
unitChoices={props.unitChoices}
/>
</View>
);
}
const styles = StyleSheet.create({});

View File

@ -1,46 +1,24 @@
import { dimensions_t, length_t } from "@/lib/dimensions_t";
import { Length } from "convert";
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
import { StyleSheet, Text, View } from "react-native";
import { NumberInput, NumberInputProps } from "./NumberInput";
export type t_length_unit = "foot" | "inch"
export type MeasurementInputProps = {
onValueSet?: (d: dimensions_t) => any,
defaultValue: length_t;
label?: string,
export type MeasurementInputProps = NumberInputProps & {
units?: Length,
}
export function MeasurementInput({onValueSet, defaultValue, label, units}: MeasurementInputProps) {
const [mValue, setMValue] = useState(defaultValue)
const defValue = Number.isNaN(defaultValue.l) ? 0 : defaultValue.l
export function MeasurementInput({onValueSet, defaultValue: defaultValue, label, units}: MeasurementInputProps) {
units = units || "ft";
function doOnValueSet(value : string) {
setMValue(mValue);
const iVal = parseFloat(value) || parseInt(value);
onValueSet && onValueSet({
...defaultValue,
l: iVal,
})
}
const sDefValue = new String(defValue).valueOf()
return (
<View style={styles.inputWrapper}>
<TextInput
clearTextOnFocus={true}
defaultValue={sDefValue}
onChangeText={doOnValueSet}
inputMode='decimal'
style={styles.lengthInput}
aria-label={label || "Enter measurement"}
/>
<Text style={styles.unitHints}>{units}</Text>
<NumberInput
onValueSet={v => onValueSet && onValueSet(v)}
defaultValue={defaultValue}
label={label}
/>
</View>
)
}

View File

@ -0,0 +1,51 @@
import { dimensions_t, length_t } from "@/lib/dimensions";
import { Length } from "convert";
import { MeasurementInput, MeasurementInputProps } from "./MeasurementInput";
import UnitChooser, {
UnitChooserPropsBase,
} from "./UnitChooser";
import { StyleSheet, View } from "react-native";
export type MeasurementUnitInputProps = MeasurementInputProps &
UnitChooserPropsBase & {
defaultValue: number;
unitChoices?: Length[];
};
export default function MeasurementUnitInput({
onValueSet,
onUnitSet,
defaultValue,
unitChoices,
defaultUnit,
label,
units,
}: MeasurementUnitInputProps) {
return (
<View style={unitChoices ? styles.inputRow : styles.inputCol}>
<MeasurementInput
onValueSet={onValueSet}
defaultValue={defaultValue}
label={label}
units={units}
/>
{unitChoices && (
<UnitChooser
defaultUnit={defaultUnit}
choices={unitChoices}
onUnitSet={onUnitSet}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
inputRow: {
flexDirection: "row",
},
inputCol: {
}
})

View File

@ -0,0 +1,54 @@
import { StyleSheet, TextInput } from "react-native";
export type NumberInputProps = {
defaultValue: number;
onValueSet: (value: number) => any;
label?: string;
};
export function NumberInput({
defaultValue,
onValueSet,
label,
}: NumberInputProps) {
const defValue = Number.isNaN(defaultValue) ? 0 : defaultValue;
function doOnValueSet(value: string) {
const iVal = parseFloat(value) || parseInt(value);
onValueSet && onValueSet(iVal);
}
const sDefValue = new String(defValue).valueOf();
return (
<TextInput
clearTextOnFocus={true}
defaultValue={sDefValue}
onChangeText={doOnValueSet}
inputMode="decimal"
style={styles.numberInput}
aria-label={label || "Enter measurement"}
/>
);
}
const styles = StyleSheet.create({
inputWrapper: {
alignItems: "flex-start",
flexDirection: "row",
verticalAlign: "middle",
},
unitHints: {
padding: 10,
fontSize: 20,
verticalAlign: "middle",
},
numberInput: {
borderWidth: 1,
borderRadius: 4,
borderColor: "grey",
padding: 4,
margin: 4,
fontSize: 25,
},
});

View File

@ -1,207 +1,197 @@
import { Product, productPriceFor } from '@/lib/product';
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';
import PriceDisplay from './Price';
import { AreaInput } from './AreaInput';
import { MeasurementInput } from './MeasurementInput';
import ProductList from './ProductList';
import UnitChooser from './UnitChooser';
import convert, { Length } from 'convert';
import PercentDamage from './PercentDamange';
import { useState, useEffect } from "react";
import { View, Text, StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import PriceDisplay from "./Price";
import { AreaInput } from "./AreaInput";
import { MeasurementInput } from "./MeasurementInput";
import ProductList from "./ProductList";
import UnitChooser from "./UnitChooser";
import convert, { Length } from "convert";
import PercentDamage from "./PercentDamange";
import MeasurementUnitInput from "./MeasurementUnitInput";
export default function ProductCalculatorSelector() {
const [activeProduct, setActiveProduct] = useState(null as Product | null);
const [price, setPrice] = useState(0);
const [measurement, setMeasurement] = useState({
l: 0,
w: 0,
u: "ft",
} as dimensions_t);
const [percentDamage, setPercentDamange] = useState(0.0);
const [activeProduct, setActiveProduct] = useState(null as Product | null);
const [price, setPrice] = useState(0);
const [measurement, setMeasurement] = useState({ l: 0, w: 0, u: "ft" } as dimensions_t);
const [percentDamage, setPercentDamange] = useState(0.0);
useEffect(
function () {
const iv = setInterval(function () {
if (!(activeProduct && measurement)) return;
setPrice(productPriceFor(activeProduct, measurement, percentDamage));
}, 50);
return function () {
clearInterval(iv);
};
},
[activeProduct, measurement, percentDamage]
);
useEffect(function () {
const iv = setInterval(function () {
if (!(activeProduct && measurement)) return;
setPrice(
productPriceFor(activeProduct, measurement, percentDamage)
function onMeasurementSet(dimensions: dimensions_t) {
setMeasurement(dimensions);
activeProduct &&
setPrice(productPriceFor(activeProduct, measurement, percentDamage));
}
function onLengthSet(l: number) {
setMeasurement({ ...measurement, l });
onMeasurementSet && onMeasurementSet({ ...measurement, l });
}
function onUnitChosen(unit: Length) {
setMeasurement({
...measurement,
u: unit,
});
}
function onSetPercentDamage(pct: number) {
setPercentDamange(pct);
}
function onProductSelected(product: Product) {
setActiveProduct(product);
setMeasurement({
l: convert(product.dimensions.l, product.dimensions.u).to(measurement.u),
u: measurement.u,
...("w" in measurement && "w" in product.dimensions
? {
w: convert(product.dimensions.w, product.dimensions.u).to(
measurement.u
),
u: measurement.u,
}
: {}),
});
}
return (
<SafeAreaView style={styles.wrapper}>
<PriceDisplay price={price} />
<View style={styles.inputAndUnitWrapper}>
<View style={styles.inputWrapper}>
{activeProduct ? (
"w" in activeProduct.dimensions ? (
<AreaInput
defaultValue={activeProduct.dimensions}
onMeasurementSet={onMeasurementSet}
widthLabel="enter width"
lengthLabel="enter length"
units={measurement.u}
/>
) : (
<MeasurementUnitInput
defaultValue={activeProduct.dimensions.l}
onValueSet={onLengthSet}
onUnitSet={onUnitChosen}
defaultUnit={activeProduct.dimensions.u}
/>
)
}, 50);
return function () {
clearInterval(iv);
};
}, [activeProduct, measurement, percentDamage]);
function onMeasurementSet(dimensions: dimensions_t) {
setMeasurement(dimensions);
activeProduct && setPrice(
productPriceFor(activeProduct, measurement, percentDamage)
)
}
function onUnitChosen(unit: Length) {
setMeasurement({
...measurement,
u: unit,
});
}
function onSetPercentDamage(pct: number) {
setPercentDamange(pct);
}
function onProductSelected(product: Product) {
setActiveProduct(product);
setMeasurement(
{
l: convert(
product.dimensions.l,
product.dimensions.u,
).to(measurement.u),
u: measurement.u,
...(
("w" in measurement && "w" in product.dimensions) ? {
w: convert(
product.dimensions.w,
product.dimensions.u,
).to(measurement.u),
u: measurement.u,
} : {}
)
}
);
}
return (
<SafeAreaView style={styles.wrapper}>
<PriceDisplay price={price} />
<View style={styles.inputAndUnitWrapper}>
<View style={styles.inputWrapper}>
{
activeProduct ? (
"w" in activeProduct.dimensions ?
<AreaInput
defaultValue={activeProduct.dimensions}
onMeasurementSet={onMeasurementSet}
widthLabel='enter width'
lengthLabel='enter length'
units={measurement.u}
/>
:
<MeasurementInput
defaultValue={activeProduct.dimensions}
onValueSet={onMeasurementSet}
label="enter length"
units={measurement.u}
/>
) : (
<Text>Please select a product</Text>
)
}
{
activeProduct && <UnitChooser choices={["in", "ft"]} onChoicePressed={onUnitChosen} />
}
</View>
</View>
{activeProduct &&
(<View style={styles.damageWrapper}>
<PercentDamage
onSetPercentage={onSetPercentDamage}
/>
</View>)
}
<ProductList onProductSelected={onProductSelected} />
</SafeAreaView>
);
) : (
<Text>Please select a product</Text>
)}
</View>
</View>
{activeProduct && (
<View style={styles.damageWrapper}>
<PercentDamage onSetPercentage={onSetPercentDamage} />
</View>
)}
<ProductList onProductSelected={onProductSelected} productType="lumber" />
</SafeAreaView>
);
}
export const styles = StyleSheet.create({
wrapper: {
overflow: "scroll"
},
bigPriceWrapper: {
alignContent: "center",
},
bigPrice: {
alignSelf: "center",
fontSize: 40,
marginTop: 100,
marginBottom: 100,
},
inputWrapper: {
flexDirection: "row",
alignItems: "flex-start",
verticalAlign: "middle",
},
unitSelector: {
},
inputAndUnitWrapper: {
flexDirection: "row",
alignSelf: "center",
},
widthInput: {
width: 200,
borderWidth: 1,
borderRadius: 4,
borderColor: "grey",
padding: 4,
margin: 4,
fontSize: 30,
},
activeProduct: {
borderWidth: 2,
borderColor: "black",
borderStyle: "solid",
},
inactiveProduct: {
wrapper: {
overflow: "scroll",
},
bigPriceWrapper: {
alignContent: "center",
},
bigPrice: {
alignSelf: "center",
fontSize: 40,
marginTop: 100,
marginBottom: 100,
},
inputWrapper: {
flexDirection: "row",
alignItems: "flex-start",
verticalAlign: "middle",
},
unitSelector: {},
inputAndUnitWrapper: {
flexDirection: "row",
alignSelf: "center",
},
widthInput: {
width: 200,
borderWidth: 1,
borderRadius: 4,
borderColor: "grey",
padding: 4,
margin: 4,
fontSize: 30,
},
activeProduct: {
borderWidth: 2,
borderColor: "black",
borderStyle: "solid",
},
inactiveProduct: {},
titleContainer: {
flexDirection: "row",
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
},
productTileTouchable: {
margin: 10,
padding: 20,
backgroundColor: "grey",
},
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
},
productTileTouchable: {
margin: 10,
padding: 20,
backgroundColor: "grey",
},
productTileTouchableActive: {
borderWidth: 2,
borderStyle: "solid",
borderColor: "black",
margin: 10,
padding: 20,
},
productTileTouchableActive: {
borderWidth: 2,
borderStyle: "solid",
borderColor: "black",
margin: 10,
padding: 20,
},
productTileText: {
textAlign: "center",
color: "white",
},
productTileText: {
textAlign: "center",
color: "white",
},
productTileTextActive: {
textAlign: "center",
color: "black",
},
productTileTextActive: {
textAlign: "center",
color: "black",
},
productTileCover: {
padding: 4,
},
damageWrapper: {
paddingVertical: 10,
paddingHorizontal: 10,
},
productTileCover: {
padding: 4,
},
damageWrapper: {
paddingVertical: 10,
paddingHorizontal: 10,
},
});

View File

@ -3,6 +3,7 @@ import { useState } from "react";
import {
Button,
FlatList,
Pressable,
StyleSheet,
Text,
TextInput,
@ -134,13 +135,13 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
</Text>
)}
</TouchableHighlight>
<TouchableHighlight
<Pressable
onPress={() => onDeleteProduct()}
aria-label="delete product"
style={styles.deleteProductHighlight}
>
<Ionicons style={styles.deleteProductButton} name="trash-outline" />
</TouchableHighlight>
</Pressable>
</View>
{showAttributes && (
<View style={styles.detailsWrapper}>

View File

@ -17,7 +17,7 @@ export default function ProductList({
const [activeProduct, setActiveProduct] = useState(null as null | Product);
const products = useAppSelector(selectProducts)
.filter((p) => !!p)
.filter((p: Product) => productType ? p.type === productType : true)
.filter((p: Product) => (!productType) || p.type === productType)
.filter((p) => {
return !!p.dimensions;
});

View File

@ -1,74 +1,84 @@
import { Length } from "convert";
import { useState } from "react";
import { Button, StyleSheet, Text, TouchableHighlight, View } from "react-native";
import {
Button,
Pressable,
StyleSheet,
Text,
TouchableHighlight,
View,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
export type UnitChooserProps = {
choices: Length[],
onChoicePressed: (l: Length) => any,
activeColor?: string,
defaultColor?: string,
defaultValue? : Length,
}
export type UnitChooserPropsBase = {
onUnitSet?: (l: Length) => any;
activeColor?: string;
defaultColor?: string;
defaultUnit?: Length;
};
export default function UnitChooser({ choices, onChoicePressed, activeColor, defaultColor, defaultValue }: UnitChooserProps) {
const [value, setValue] = useState(defaultValue || choices[0] as Length);
export type UnitChooserProps = UnitChooserPropsBase & {
choices: Length[];
};
activeColor = activeColor || "lightblue";
defaultColor = defaultColor || "lightgrey";
export default function UnitChooser({
choices,
onUnitSet,
activeColor,
defaultColor,
defaultUnit,
}: UnitChooserProps) {
const [value, setValue] = useState(defaultUnit || (choices[0] as Length));
function doChoiceClicked(choice: Length) {
setValue(choice);
onChoicePressed(choice);
}
activeColor = activeColor || "lightblue";
defaultColor = defaultColor || "lightgrey";
return (
<View style={styles.unitChooser}>
{choices.map((ci) => {
return (
<TouchableHighlight
onPress={() => doChoiceClicked(ci)}
style={value === ci ? styles.active : styles.default }
key={ci}
>
<Text style={value === ci ? styles.textActive : styles.textDefault}>{ci}</Text>
</TouchableHighlight>
)
})
}
</View>
)
function doChoiceClicked(choice: Length) {
setValue(choice);
onUnitSet && onUnitSet(choice);
}
const activeColors = ['#a7caff', '#5588ff', '#5588ff', '#5588ff'];
const inactiveColors = ['#d0d0d0', '#828282', '#828282', '#828282'];
return (
<View style={styles.unitChooser}>
{choices.map((ci) => {
return (
<LinearGradient
colors={ci === value ? activeColors : inactiveColors}
style={styles.gradientButton}
>
<TouchableHighlight style={{padding: 5, borderRadius: 5, }} onPress={() => doChoiceClicked(ci)} key={ci}>
<Text style={{padding: 5, fontSize: 16}}>{ci}</Text>
</TouchableHighlight>
</LinearGradient>
);
})}
</View>
);
}
const styles = StyleSheet.create({
unitChooser: {
flexDirection: "row",
verticalAlign: "middle",
},
active: {
backgroundColor: "skyblue",
padding: 5,
borderRadius: 5,
},
default: {
backgroundColor: "lightgray",
padding: 5,
borderRadius: 5,
verticalAlign: "middle",
},
textActive: {
marginTop: 2,
marginBottom: 2,
marginLeft: 10,
marginRight: 10,
fontSize: 25,
},
textDefault: {
marginTop: 2,
marginBottom: 2,
marginLeft: 10,
marginRight: 10,
fontSize: 25,
},
unitButton: {
},
})
gradientButton: {},
unitChooser: {
flexDirection: "row",
verticalAlign: "middle",
padding: 4,
},
textActive: {
marginTop: 2,
marginBottom: 2,
marginLeft: 10,
marginRight: 10,
fontSize: 25,
},
textDefault: {
marginTop: 2,
marginBottom: 2,
marginLeft: 10,
marginRight: 10,
fontSize: 25,
},
unitButton: {},
});

View File

@ -1,5 +1,11 @@
import React from "react";
import { render, fireEvent, screen, within } from "@testing-library/react-native";
import {
render,
fireEvent,
screen,
within,
act,
} from "@testing-library/react-native";
import CarpetRollCalculator from "@/components/CarpetRollCalculator";
import { renderWithProviders } from "@/lib/rendering";
@ -7,6 +13,8 @@ import allProducts from "@/__fixtures__/initialProducts";
import { Product, pricePerUnitDisplay } from "@/lib/product";
import initialProducts from "@/__fixtures__/initialProducts";
jest.useFakeTimers();
const areaRugProducts = allProducts.filter((p) => "area_rug" === p.type);
describe("CarpetRollCalculator", () => {
@ -15,30 +23,42 @@ describe("CarpetRollCalculator", () => {
products: initialProducts,
});
const areaRug = initialProducts.find(p => p.type === 'area_rug') as Product;
const areaRug = initialProducts.find(
(p) => p.type === "area_rug"
) as Product;
const areaRugLabel = `product ${areaRug.id}`;
fireEvent.press(screen.getByLabelText(areaRugLabel));
act(() => {
fireEvent.press(screen.getByLabelText(areaRugLabel));
});
// Test the interaction with the width input
const widthInput = screen.getByLabelText("width");
fireEvent.changeText(widthInput, "10");
act(() => {
fireEvent.changeText(widthInput, "10");
});
// Test the interaction with the outer diameter input
const outerDiameterInput = screen.getByLabelText("outer diameter");
fireEvent.changeText(outerDiameterInput, "3");
act(() => {
fireEvent.changeText(outerDiameterInput, "3");
});
// Test the interaction with the inner diameter input
const innerDiameterInput = screen.getByLabelText("inner diameter");
fireEvent.changeText(innerDiameterInput, "1");
act(() => {
fireEvent.changeText(innerDiameterInput, "1");
});
// Test the interaction with the number of rings input
const numRingsInput = screen.getByLabelText("number of rings");
fireEvent.changeText(numRingsInput, "5");
act(() => {
fireEvent.changeText(numRingsInput, "5");
});
jest.advanceTimersByTime(3000);
// Test the interaction with the price display
const {getByText} = within(screen.getByLabelText("area rug price"));
const { getByText } = within(screen.getByLabelText("area rug price"));
expect(getByText(/\$.*58.*\..*19.*/)).toBeTruthy();
});
});

View File

@ -1,49 +1,51 @@
import { LumberProduct, Product } from "@/lib/product"
import {ProductAttributeEditor} from "../ProductAttributeEditor"
import { fireEvent, render, screen } from '@testing-library/react-native';
import { LumberProduct, Product } from "@/lib/product";
import { ProductAttributeEditor } from "../ProductAttributeEditor";
import { fireEvent, render, screen } from "@testing-library/react-native";
import { renderWithProviders } from "@/lib/rendering";
describe("Product editor tests", () => {
const productName = "Fun Product";
it("Product attributes can be deleted", async () => {
const onChange = jest.fn();
const onDelete = jest.fn();
render(
<ProductAttributeEditor
attributeKey="name"
attributeValue="product"
onChangeAttribute={onChange}
onDelete={onDelete}
/>);
expect(screen.getByLabelText("Delete Attribute")).not.toBeNull();
fireEvent.press(await screen.getByLabelText("Delete Attribute"));
expect(onDelete).toHaveBeenCalled();
});
it("Product attributes can be modified", async () => {
const product : Product = {
pricePerUnit: 10,
dimensions: {
l: 40,
u: "ft",
},
type: "lumber",
}
const onChange = jest.fn();
const onDelete = jest.fn();
const onKeyChange = jest.fn();
render(
<ProductAttributeEditor
attributeKey="old test key"
attributeValue="old test value"
onChangeAttribute={onChange}
onDelete={onDelete}
onChangeAttributeKey={onKeyChange}
/>);
fireEvent.changeText(screen.getByLabelText("Edit Key"), "new test key");
expect(onKeyChange).toHaveBeenCalled();
fireEvent.changeText(screen.getByLabelText("Edit Value"), "new name");
expect(onChange).toHaveBeenCalled();
fireEvent.press(screen.getByLabelText("Delete Attribute"));
expect(onDelete).toHaveBeenCalled();
})
})
const productName = "Fun Product";
it("Product attributes can be deleted", async () => {
const onChange = jest.fn();
const onDelete = jest.fn();
renderWithProviders(
<ProductAttributeEditor
attributeKey="name"
attributeValue="product"
onChangeAttribute={onChange}
onDelete={onDelete}
/>
);
expect(screen.getByLabelText("Delete Attribute")).not.toBeNull();
fireEvent.press(await screen.getByLabelText("Delete Attribute"));
expect(onDelete).toHaveBeenCalled();
});
it("Product attributes can be modified", async () => {
const product: Product = {
pricePerUnit: 10,
dimensions: {
l: 40,
u: "ft",
},
type: "lumber",
};
const onChange = jest.fn();
const onDelete = jest.fn();
const onKeyChange = jest.fn();
render(
<ProductAttributeEditor
attributeKey="old test key"
attributeValue="old test value"
onChangeAttribute={onChange}
onDelete={onDelete}
onChangeAttributeKey={onKeyChange}
/>
);
fireEvent.changeText(screen.getByLabelText("Edit Key"), "new test key");
expect(onKeyChange).toHaveBeenCalled();
fireEvent.changeText(screen.getByLabelText("Edit Value"), "new name");
expect(onChange).toHaveBeenCalled();
fireEvent.press(screen.getByLabelText("Delete Attribute"));
expect(onDelete).toHaveBeenCalled();
});
});

View File

@ -3,9 +3,10 @@ import { Provider } from 'react-redux';
import ProductCalculatorSelector from '@/components/ProductCalculatorSelector';
import { renderWithProviders } from '@/lib/rendering';
import { Product, pricePerUnitDisplay, productPriceFor } from '@/lib/product';
import initialProducts from '@/__fixtures__/initialProducts';
jest.useFakeTimers();
const mockAreaProduct = initialProducts.find(p => 'w' in p.dimensions ) as Product
const mockLengthProduct = initialProducts.find(p => (!('w' in p.dimensions)) ) as Product
@ -40,13 +41,17 @@ describe('ProductCalculatorSelector', () => {
expect(screen.getByText('Please select a product')).toBeTruthy();
const areaLabel = `${mockAreaProduct.attributes?.name} (${pricePerUnitDisplay(mockAreaProduct)})`;
fireEvent.press(screen.getByText(areaLabel));
act(()=>{
fireEvent.press(screen.getByText(areaLabel));
})
const lengthInput = screen.getByLabelText("enter length");
const widthInput = screen.getByLabelText("enter length");
expect(lengthInput).toBeTruthy();
expect(widthInput).toBeTruthy();
fireEvent.press(screen.getByText("in"));
act(() => {
fireEvent.press(screen.getByText("in"));
})
act(() => {
fireEvent.changeText(lengthInput, "2");
@ -59,6 +64,6 @@ describe('ProductCalculatorSelector', () => {
const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,});
const element = screen.getByLabelText("calculated price");
const {getByText} = within(element);
expect(getByText(/\$.*0.*\.10/)).toBeTruthy();
expect(getByText(/\$.*15.*\.00/)).toBeTruthy();
});
});

View File

@ -2,21 +2,13 @@ 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 { LumberProduct, Product } from "@/lib/product";
import { LumberProduct, Product, productLabel } from "@/lib/product";
import initialProducts from "@/__fixtures__/initialProducts";
describe("ProductEditor", () => {
const productName = "Flooring";
const mockProduct: LumberProduct = {
attributes: {
name: productName,
},
pricePerUnit: 10,
dimensions: {
l: 40,
u: "ft",
},
type: "lumber",
};
const mockProduct = initialProducts[0];
it("renders correctly", async () => {
const { store } = renderWithProviders(<ProductEditor />, {
products: [mockProduct],
@ -30,11 +22,15 @@ describe("ProductEditor", () => {
// Check if the product names are rendered
expect(
screen.getByText(products[0].attributes.name as string)
screen.getByText(mockProduct.attributes?.name as string)
).toBeTruthy();
const label = productLabel(mockProduct);
// Start to edit a product
fireEvent.press(screen.getByText(productName));
act(() => {
fireEvent.press(screen.getByText(label));
})
// Change properties of the product to make sure it's updated in the store
@ -50,7 +46,9 @@ describe("ProductEditor", () => {
expect(products[0].dimensions.w).toBe(32);
fireEvent.press(screen.getByLabelText("delete product"));
act(() => {
fireEvent.press(screen.getByLabelText("delete product"));
})
products = selectProducts(store.getState());
expect(products.length).toBe(0);
});

View File

@ -9,7 +9,7 @@ describe('UnitChooser', () => {
it('renders correctly', () => {
const { getByText } = render(
<UnitChooser choices={choices} onChoicePressed={mockOnChoicePressed} />
<UnitChooser choices={choices} onUnitSet={mockOnChoicePressed} />
);
choices.forEach(choice => {
@ -19,7 +19,7 @@ describe('UnitChooser', () => {
it('calls onChoicePressed when a button is pressed', () => {
const { getByText } = render(
<UnitChooser choices={choices} onChoicePressed={mockOnChoicePressed} />
<UnitChooser choices={choices} onUnitSet={mockOnChoicePressed} />
);
fireEvent.press(getByText(choices[0]));