good enough for government (or habitat) work

This commit is contained in:
Jordan 2024-07-01 12:23:45 -07:00
parent 379f43dcd9
commit ecdc9db085
18 changed files with 325 additions and 169 deletions

View File

@ -2,11 +2,20 @@ import { Product } from "@/lib/product";
export const products = [
// Sheet goods
new Product(25, {l: 4, w : 8, u: "feet"}, { name: "Plywood" }),
new Product(35, {l: 4, w : 8, u: "feet"}, { name: "MDF" }),
new Product(40, {l: 4, w : 8, u: "feet"}, { name: "OSB" }),
new Product(45, {l: 4, w : 8, u: "feet"}, { name: "Sheetrock" }),
// Beams and trim
new Product(1, {l: 0.50, u : "feet"}, { name: "trim 3 inches" }),
new Product(1, {l: 0.75, u : "feet"}, { name: "trim 3 inches" }),
new Product(25, {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"}),
];

View File

@ -22,9 +22,9 @@ export default function TabLayout() {
<Tabs.Screen
name="index"
options={{
title: 'Conversion',
title: 'Home Screen',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? 'recording' : 'recording-outline'} color={color} />
<TabBarIcon name={focused ? 'scale' : 'scale-outline'} color={color} />
),
}}
/>
@ -33,7 +33,7 @@ export default function TabLayout() {
options={{
title: 'Products',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? 'recording' : 'recording-outline'} color={color} />
<TabBarIcon name={focused ? 'list' : 'list-outline'} color={color} />
),
}}
/>

View File

@ -1,13 +1,10 @@
import ProductCalculatorSelector from '@/components/ProductCalculatorSelector';
import { SafeAreaView, View } from 'react-native';
import { SafeAreaView, Text, View } from 'react-native';
const fallbackImage = require("@/assets/images/board-stock-lightened-blurred.png");
export const HomeScreen = () => {
export default function Convert () {
return (
<SafeAreaView>
<View>
<ProductCalculatorSelector />
</SafeAreaView>
</View>
)
}

View File

@ -1,7 +1,7 @@
import { MeasurementInput } from "./MeasurementInput";
import { area_t, dimensions_t } from "@/lib/product";
import { useState } from "react";
import { View } from "react-native";
import { StyleSheet, View } from "react-native";
export type AreaInputProps = {
onMeasurementSet?: (area : dimensions_t) => any,
@ -12,7 +12,7 @@ export type AreaInputProps = {
export function AreaInput({onMeasurementSet, lengthLabel, widthLabel, defaultValue} : AreaInputProps) {
defaultValue = defaultValue || {l: 0, w: 0, u: "foot"}
defaultValue = defaultValue || {l: 0, w: 0, u: "ft"}
const [area, setArea] = useState(defaultValue)
@ -39,19 +39,23 @@ export function AreaInput({onMeasurementSet, lengthLabel, widthLabel, defaultVal
}
return (
<View>
<View style={styles.areaInputWrapper}>
<MeasurementInput
units={area.u}
defaultValue={area.l}
defaultValue={{l: area.l, u: area.u}}
onValueSet={doOnLengthSet}
label={lengthLabel}
/>
<MeasurementInput
units={area.u}
defaultValue={area.w}
defaultValue={{l: area.w, u: area.u}}
onValueSet={doOnWidthSet}
label={widthLabel}
/>
</View>
)
}
const styles = StyleSheet.create({
areaInputWrapper: {
flexDirection: "row"
}
})

View File

@ -1,5 +1,6 @@
import { dimensions_t, length_t } from "@/lib/product";
import { Length } from "convert";
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
export type t_length_unit = "foot" | "inch"
@ -11,8 +12,12 @@ export type MeasurementInputProps = {
}
export function MeasurementInput({onValueSet, defaultValue, label}: MeasurementInputProps) {
1
const [mValue, setMValue] = useState(defaultValue)
const defValue = Number.isNaN(defaultValue.l) ? 0 : defaultValue.l
function doOnValueSet(value : string) {
setMValue(mValue);
const iVal = parseFloat(value) || parseInt(value);
onValueSet && onValueSet({
...defaultValue,
@ -20,10 +25,10 @@ export function MeasurementInput({onValueSet, defaultValue, label}: MeasurementI
})
}
const sDefValue = new String(defaultValue.l).valueOf()
const sDefValue = new String(defValue).valueOf()
return (
<View>
<View style={styles.inputWrapper}>
<TextInput
clearTextOnFocus={true}
defaultValue={sDefValue}
@ -32,14 +37,17 @@ export function MeasurementInput({onValueSet, defaultValue, label}: MeasurementI
style={styles.lengthInput}
aria-label={label || "Enter measurement"}
/>
<Text style={styles.unitHints}>{defaultValue.u}</Text>
<Text style={styles.unitHints}>{mValue.u}</Text>
</View>
)
}
const styles = StyleSheet.create({
inputWrapper: {
alignItems: "flex-start",
flexDirection: "row"
},
unitHints: {
fontSize: 30,
padding: 10,
},
lengthInput: {
@ -48,7 +56,7 @@ const styles = StyleSheet.create({
borderColor: "grey",
padding: 4,
margin: 4,
fontSize: 30,
width: 200,
fontSize: 25,
width: 100,
},
})

View File

@ -0,0 +1,44 @@
import { StyleSheet, Text, TextInput, View } from "react-native";
import Slider from '@react-native-community/slider';
import { useState } from "react";
type PercentDamageProps = {
onSetPercentage: (percent: number) => any;
}
export default function PercentDamage ({onSetPercentage} : PercentDamageProps) {
const [damage, setDamage] = useState(0);
function doOnChangeText (val : number) {
setDamage(val);
onSetPercentage(val / 100);
}
return (
<View style={styles.wrapper}>
<Slider
value={damage}
minimumValue={0}
maximumValue={100}
step={5}
onValueChange={doOnChangeText}
/>
<Text style={styles.label}> {damage}% Damage</Text>
</View>
)
}
const styles = StyleSheet.create({
wrapper: {
padding: 5,
},
input: {
flex: 1,
margin: 5,
padding: 5,
borderWidth: 2,
borderColor: "lightgrey",
borderStyle: "solid",
},
label: {
margin: 5,
}
})

View File

@ -32,7 +32,7 @@ export const styles = StyleSheet.create({
bigPrice: {
alignSelf: "center",
fontSize: 40,
marginTop: 100,
marginBottom: 100,
marginTop: 50,
marginBottom: 50,
}
});

View File

@ -1,48 +1,75 @@
import { useAppSelector } from '@/app/store';
import { selectProducts } from '@/features/product/productSlice';
import { Product, dimensions_t } from '@/lib/product';
import { useState, useEffect } from 'react';
import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native';
import { TouchableHighlight } from 'react-native-gesture-handler';
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 { Length } from 'convert';
import convert, { Length } from 'convert';
import PercentDamage from './PercentDamange';
export default function ProductCalculatorSelector() {
const products = useAppSelector(selectProducts);
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 [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(
activeProduct.priceFor(measurement)
activeProduct.priceFor(measurement, percentDamage)
)
}, 50);
return function () {
clearInterval(iv);
};
}, [activeProduct, measurement]);
}, [activeProduct, measurement, percentDamage]);
function onMeasurementSet(dimensions: dimensions_t) {
setMeasurement(dimensions);
activeProduct && setPrice(
activeProduct.priceFor(measurement, percentDamage)
)
}
function onUnitChosen(unit : Length) {
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} />
@ -73,7 +100,14 @@ export default function ProductCalculatorSelector() {
}
</View>
</View>
<ProductList onProductSelected={setActiveProduct} />
{activeProduct &&
(<View >
<PercentDamage
onSetPercentage={onSetPercentDamage}
/>
</View>)
}
<ProductList onProductSelected={onProductSelected} />
</SafeAreaView>
);
}
@ -81,6 +115,7 @@ export default function ProductCalculatorSelector() {
export const styles = StyleSheet.create({
wrapper: {
overflow: "scroll"
},
bigPriceWrapper: {
alignContent: "center",

View File

@ -39,8 +39,8 @@ export const ProductEditor = ({}) => {
}
return (
<SafeAreaView>
<h1 style={styles.h1}>Edit Products</h1>
<SafeAreaView style={{overflow: "scroll"}}>
<Text>Edit Products</Text>
<FlatList
data={products}
keyExtractor={(p, i) => `product-${p.id}`}

View File

@ -1,12 +1,10 @@
import { Id, Product, dimensions_t } from "@/lib/product"
import { useState } from "react"
import { Button, FlatList, StyleSheet, Text, Touchable, TouchableHighlight, View } from "react-native"
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
import { ProductAttributeEditor } from "./ProductAttributeEditor";
import { TextInput } from "react-native-gesture-handler";
import { useAppSelector } from "@/app/store";
import rfdc from "rfdc";
import SelectDropdown from "react-native-select-dropdown";
import { Dropdown } from 'react-native-element-dropdown';
import { Ionicons } from "@expo/vector-icons";
import { Length } from "convert";
export type ProductAddedFunc = () => any;
export type ProductDeletedFunc = (product_id: Id) => any;
@ -50,9 +48,9 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
props.onPriceUpdated && props.onPriceUpdated(product.id, parseFloat(pricePerUnit) || parseInt(pricePerUnit));
}
function onUnitsChanged(newUnits: "foot" | "inch") {
function onUnitsChanged(newUnits: Length) {
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
...((product.area || product.length) as dimensions_t),
...(product.dimensions as dimensions_t),
u: newUnits,
})
}
@ -60,16 +58,16 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
function onChangeLength(len: string) {
const l = parseFloat(len) || parseInt(len);
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
...((product.area || product.length) as dimensions_t),
...(product.dimensions as dimensions_t),
l,
})
}
function onChangeWidth(width: string) {
const w = parseFloat(width) || parseInt(width);
const w = width.length == 0 ? null : parseFloat(width) || parseInt(width);
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
...((product.area || product.length) as dimensions_t),
w,
...(product.dimensions as dimensions_t),
...(w ? {w} : {}),
})
}
@ -77,31 +75,36 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
props.onProductDeleted && props.onProductDeleted(product.id);
}
const length = new String(product.area?.l || product.length?.l || "0") as string;
const width = new String(product.area?.w || "") as string;
const dimension = product.area?.u || product.length?.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>
<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}
<View style={styles.productListHeader}>
<TouchableHighlight
onPress={() => setShowAttributes(!showAttributes)}
aria-label="Product Item"
style={styles.productItemName}
>
<Ionicons style={styles.deleteProductButton} name="trash-outline" />
</TouchableHighlight>
<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>
<View style={styles.detailsWrapper}>
<View style={styles.priceSpecWrapper}>
<Text style={styles.priceLabel}></Text>
<Text style={styles.priceLabel}>$</Text>
<TextInput inputMode="decimal"
defaultValue={new String(product.pricePerUnit) as string}
aria-label="price per unit"
@ -109,14 +112,18 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
style={styles.priceInput}
/>
<Text style={styles.per}>per</Text>
<Text style={styles.unitsLabel}>Units: </Text>
<select
onChange={(e) => onUnitsChanged(e.target.value as "foot" | "inch")}
<Dropdown
data={[
{label: "feet", value: "ft"},
{label: "inches", value: "in"},
]}
style={styles.unitsSelect}
aria-label="units">
<option value="foot" selected={dimension === "foot"}>feet</option>
<option value="inch" selected={dimension === "inch"}>inches</option>
</select>
mode="modal"
labelField="label"
valueField="value"
value={product.dimensions.u || "ft"}
onChange={(item) => onUnitsChanged(item.value as Length)}
/>
<TextInput
inputMode="decimal"
defaultValue={length}
@ -124,7 +131,7 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
style={styles.lengthInput}
aria-label="length"
/>
<Text>x</Text>
<Text style={{flex: 1,}}>x</Text>
<TextInput
inputMode="decimal"
defaultValue={width}
@ -158,33 +165,50 @@ export const ProductEditorItem = (props: ProductEditorItemProps) => {
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,

View File

@ -1,47 +1,46 @@
import { FlatList, StyleSheet, Text, TouchableHighlight } from "react-native";
import { FlatList, ScrollView, StyleSheet, Text, TouchableHighlight } from "react-native";
import { ProductTile } from "./ProductTile";
import { Product } from "@/lib/product";
import { useState } from "react";
import { Id, Product } from "@/lib/product";
import { Key, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { selectProducts } from "@/features/product/productSlice";
import { useAppSelector } from "@/app/store";
export type ProductSelectionProps = {
onProductSelected?: (product : Product) => any;
onProductSelected?: (product: Product) => any;
}
export default function ProductList ({onProductSelected} : ProductSelectionProps) {
export default function ProductList({ onProductSelected }: ProductSelectionProps) {
const products = useSelector(selectProducts);
const [activeProduct, setActiveProduct] = useState(null as null | Product);
const products = useAppSelector(selectProducts).filter(p => (!!p.dimensions));
const [activeProduct, setActiveProduct] = useState(null as null | Product);
function doOnProductSelected(product: Product) {
setActiveProduct(product);
onProductSelected && onProductSelected(product);
}
function doOnProductSelected(product : Product) {
setActiveProduct(product);
onProductSelected && onProductSelected(product);
}
return (
<FlatList
data={products}
style={styles.productSelectorFlatList}
renderItem={({ item }) => {
return (
<ScrollView scrollToOverflowEnabled={true}>
{products.map(product => {
return (
<ProductTile
product={item}
onProductSelected={doOnProductSelected}
isActive={activeProduct === item}
/>
<ProductTile
product={product}
onProductSelected={doOnProductSelected}
isActive={activeProduct === product}
key={product.id}
/>
);
} } />
)
})}
</ScrollView>
)
}
const styles = StyleSheet.create({
productSelectorFlatList: {
padding: 10,
margin: 10,
},
productSelectorFlatList: {
padding: 10,
margin: 10,
},
})

View File

@ -6,49 +6,50 @@ export type OnProductSelectedFunc = (product : Product) => any;
type MyStyle = StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
type StyleSpec = {
highlight?: MyStyle,
text?: MyStyle,
image?: MyStyle,
}
export type ProductTileProps = {
product: (Product),
onProductSelected?: OnProductSelectedFunc,
isActive: boolean,
style?: {
default?: {
highlight?: MyStyle,
text?: MyStyle,
image?: MyStyle,
}
active?: {
highlight?: MyStyle,
text?: MyStyle,
image?: MyStyle,
}
}
}
const FALLBACK_IMAGE = "";
export function ProductTile ({product, onProductSelected, isActive, style} : ProductTileProps) {
const _style = (isActive ? style?.active : style?.default) || {};
export function ProductTile ({product, onProductSelected, isActive} : ProductTileProps) {
const k = isActive ? "active" : "default";
return (
<TouchableHighlight
style={_style.highlight || styles.highlight}
style={styles[k].highlight}
onPress={() => onProductSelected && onProductSelected(product)}>
<Text style={_style.text || styles.text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
<Text style={styles[k].text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
</TouchableHighlight>
);
}
const styles = StyleSheet.create({
highlight: {
},
image: {
},
text: {
},
tile: {
},
})
const styles = {
active: StyleSheet.create({
highlight: {
padding: 10,
margin: 2,
color: "lightblue",
},
text: {
}
}),
default: StyleSheet.create({
highlight: {
padding: 10,
margin: 2,
backgroundColor: "lightgrey",
},
text: {
}
}),
}

View File

@ -13,7 +13,7 @@ export default function UnitChooser({ choices, onChoicePressed, activeColor, def
const [value, setValue] = useState(choices[0] as Length);
activeColor = activeColor || "lightblue";
defaultColor = activeColor || "lightgrey";
defaultColor = defaultColor || "lightgrey";
function doChoiceClicked(choice: Length) {
setValue(choice);

View File

@ -1,4 +1,4 @@
import { render, fireEvent, screen, act } from '@testing-library/react-native';
import { render, fireEvent, screen, act, within } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import ProductCalculatorSelector from '@/components/ProductCalculatorSelector';
import { renderWithProviders } from '@/lib/rendering';
@ -61,10 +61,12 @@ describe('ProductCalculatorSelector', () => {
fireEvent.changeText(widthInput, "4");
});
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(3000);
const price = mockAreaProduct.priceFor({l: 2, w: 4, u: "ft"});
const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,});
expect(screen.getByLabelText("calculated price").find().toBeTruthy();
const element = screen.getByLabelText("calculated price");
const {getByText} = within(element);
expect(getByText(sPrice)).toBeTruthy();
});
});

View File

@ -146,10 +146,14 @@ const productsState = createSlice({
}
});
export const selectProducts = (state: RootState) => {
return state.products.map(obj => Product.fromObject(obj));
export const selectProductsDatas = (state: RootState) => {
return state.products;
}
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

@ -66,7 +66,7 @@ export function dimensionArea(d: dimensions_t) {
export class Product {
public id? : Id;
public id?: Id;
constructor(public pricePerUnit: number, public dimensions: dimensions_t, public attributes: ProductAttributes = {},
id?: Id,
@ -74,12 +74,13 @@ export class Product {
this.id = id || uuid.v4().toString();
}
public priceFor(dimensions: dimensions_t): number {
public priceFor(dimensions: dimensions_t, damage : number): number {
if (Number.isNaN(damage)) damage = 0;
const dim = matchDimensions(dimensions, this.dimensions);
return (
dim.w ? dimensionArea(dim) / dimensionArea(this.dimensions) * this.pricePerUnit
: (dim.l / this.dimensions.l) * this.pricePerUnit
)
) * (1.0 - damage);
}
get priceDisplay() {
@ -91,9 +92,9 @@ export class Product {
get pricePerUnitDisplay() {
const p = this.priceDisplay;
const {l, u} = this.dimensions;
const { l, u } = this.dimensions;
const w = (this.dimensions as area_t).w || null;
const d = w ? `${l}${u} x ${l}${u}` : `${l}${u}`;
const d = w ? `${l}${u} x ${w}${u}` : `${l}${u}`;
return `$${p} per ${d}`
}
@ -113,16 +114,16 @@ export class Product {
);
}
get asObject() : ProductData {
get asObject(): ProductData {
return {
id: this.id,
pricePerUnit: this.pricePerUnit,
dimensions: JSON.parse(JSON.stringify(this.dimensions)),
dimensions: this.dimensions,
attributes: this.attributes,
}
}
static fromObject({id, pricePerUnit, dimensions, attributes} : ProductData) {
static fromObject({ id, pricePerUnit, dimensions, attributes }: ProductData) {
return new Product(
pricePerUnit,
dimensions,

View File

@ -5,7 +5,7 @@
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"android": "expo start --android --offline",
"ios": "expo start --ios",
"web": "expo start --web --offline",
"test": "jest --watchAll",
@ -15,9 +15,11 @@
"@babel/runtime": "^7.24.7",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-native-community/slider": "^4.5.2",
"@react-native/assets-registry": "^0.74.84",
"@react-navigation/native": "^6.1.17",
"@reduxjs/toolkit": "^2.2.5",
"@slider": "link:@react-native-community/@slider",
"@testing-library/react-native": "^12.5.1",
"@types/js-quantities": "^1.6.6",
"class-transformer": "^0.5.1",
@ -38,6 +40,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.2",
"react-native-element-dropdown": "^2.12.1",
"react-native-flex-grid": "^1.0.4",
"react-native-gesture-handler": "~2.16.2",
"react-native-reanimated": "~3.10.1",

View File

@ -14,6 +14,9 @@ dependencies:
'@react-native-async-storage/async-storage':
specifier: ^1.23.1
version: 1.23.1(react-native@0.74.2)
'@react-native-community/slider':
specifier: ^4.5.2
version: 4.5.2
'@react-native/assets-registry':
specifier: ^0.74.84
version: 0.74.84
@ -23,6 +26,9 @@ dependencies:
'@reduxjs/toolkit':
specifier: ^2.2.5
version: 2.2.5(react-redux@9.1.2)(react@18.2.0)
'@slider':
specifier: link:@react-native-community/@slider
version: link:@react-native-community/@slider
'@testing-library/react-native':
specifier: ^12.5.1
version: 12.5.1(jest@29.7.0)(react-native@0.74.2)(react-test-renderer@18.2.0)(react@18.2.0)
@ -83,6 +89,9 @@ dependencies:
react-native:
specifier: 0.74.2
version: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7)(@types/react@18.2.79)(react@18.2.0)
react-native-element-dropdown:
specifier: ^2.12.1
version: 2.12.1(react-native@0.74.2)(react@18.2.0)
react-native-flex-grid:
specifier: ^1.0.4
version: 1.0.4(react-native@0.74.2)(react@18.2.0)
@ -2718,6 +2727,10 @@ packages:
- utf-8-validate
dev: false
/@react-native-community/slider@4.5.2:
resolution: {integrity: sha512-DbFyCyI7rwl0FkBkp0lzEVp+5mNfS5qU/nM2sK2aSguWhj0Odkt1aKHP2iW/ljruOhgS/O4dEixXlne4OdZJDQ==}
dev: false
/@react-native/assets-registry@0.74.84:
resolution: {integrity: sha512-dzUhwyaX04QosWZ8zyaaNB/WYZIdeDN1lcpfQbqiOhZJShRH+FLTDVONE/dqlMQrP+EO7lDqF0RrlIt9lnOCQQ==}
engines: {node: '>=18'}
@ -8771,6 +8784,18 @@ packages:
/react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
/react-native-element-dropdown@2.12.1(react-native@0.74.2)(react@18.2.0):
resolution: {integrity: sha512-Z3uWNFBoezDEsy9AZJxoDc9DxoAdfeprUjaInmbuzYOk6R0Y0UZ659JIalX20XNvrNRWJUfSZwbM94jWYNsIyw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
react: '*'
react-native: '*'
dependencies:
lodash: 4.17.21
react: 18.2.0
react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7)(@types/react@18.2.79)(react@18.2.0)
dev: false
/react-native-flex-grid@1.0.4(react-native@0.74.2)(react@18.2.0):
resolution: {integrity: sha512-VFadQy3JpgBM2fNsn7W/TdebZ0JNeZgedxPJ0Xi6o+HQJU8j43YGUsGot72rEMWlzaAJCGQnMQXkW9vX+E2n5w==}
peerDependencies: