diff --git a/app/(tabs)/home/_layout.tsx b/app/(tabs)/home/_layout.tsx index 42d452e..abe83bf 100644 --- a/app/(tabs)/home/_layout.tsx +++ b/app/(tabs)/home/_layout.tsx @@ -12,10 +12,10 @@ export default function _Layout() { initialRouteName="index" screenOptions={{ contentStyle: { - backgroundColor:colors.backgroundColor, + backgroundColor:colors.containerColor, }, headerStyle: { - backgroundColor: colors.backgroundColor + backgroundColor: colors.containerColor }, headerTintColor: colors.primaryText diff --git a/app/(tabs)/home/addItem.tsx b/app/(tabs)/home/addItem.tsx index d403a23..d7d24f2 100644 --- a/app/(tabs)/home/addItem.tsx +++ b/app/(tabs)/home/addItem.tsx @@ -1,26 +1,93 @@ -import { View, Text, StyleSheet, TextInput, NativeSyntheticEvent, TextInputKeyPressEventData, TouchableOpacity } from 'react-native' +import { View, Text, StyleSheet, Alert } from 'react-native' import React, { useRef, useState } from 'react' import { SIZES } from '../../../constants/theme' import { useTheme } from '../../contexts/ThemeContext' -import { AutoDecimalInput } from '../../../components' +import { AutoDecimalInput, CategorySelector, CategorySelectorModal, DateSelectorButton, RoundedButton, TextInputBar } from '../../../components' +import { Category } from '../../../types/dbItems' +import DateTimePicker from '@react-native-community/datetimepicker'; +import { addExpense } from '../../../services/database' +import { SimpleDate } from '../../../util/SimpleDate' +import { useRouter } from 'expo-router' export default function AddItem() { - const value = useRef(""); + const {colors} = useTheme(); + const router = useRouter(); + const [formatedValue, setFormatedValue] = useState(""); + const [selectorModalVisible, setSelecorModalVisible] = useState(false); + const [selectedCategory, setSelectedCategory] = useState() + const [expenseName, setExpenseName] = useState(""); + const [datePickerShown, setDatePickerShown] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()) const handleValueChange = (formatedValue: string) => { setFormatedValue(formatedValue); } - console.log(formatedValue) + + const handleCategorySelect = (category : Category) => { + setSelecorModalVisible(false); + setSelectedCategory(category); + } + + const validateInput = ():boolean => { + if(formatedValue == "" || expenseName == "" || selectedCategory === undefined || selectedDate === null){ + return false; + } + return true; + } + + const submit = () => { + const insert = async () => { + await addExpense(expenseName, selectedCategory?.guid!, new SimpleDate(selectedDate).format("YYYY-MM-DD"), Number(formatedValue)) + } + if(validateInput()){ + insert(); + router.back(); + }else { + Alert.alert("Invalid input", "One of the Props is not properly defined") + } + + } + return ( + {setSelecorModalVisible(false)}} onCategoryTap={handleCategorySelect}> + {setSelecorModalVisible(true)}} selectedCategory={selectedCategory}/> + setExpenseName(text)}/> + {setDatePickerShown(true)}}/> + {datePickerShown && + { + setDatePickerShown(false); + if(date){ + setSelectedDate(date); + } + }} + />} + + Save + ) } const styles = StyleSheet.create({ + save: { + marginTop: 40, + padding: 10, + }, container: { margin: SIZES.normal, + display: "flex", + gap: 10 + }, + + submitText: { + fontSize: SIZES.large } + }) \ No newline at end of file diff --git a/app/(tabs)/home/index.tsx b/app/(tabs)/home/index.tsx index edd6794..5a0a5a8 100644 --- a/app/(tabs)/home/index.tsx +++ b/app/(tabs)/home/index.tsx @@ -1,9 +1,9 @@ import React, { useRef, useState, useMemo } from 'react'; import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native'; import { Calendar } from 'react-native-calendars'; -import { FlatList } from 'react-native-gesture-handler'; +import { FlatList, RefreshControl } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { ExpenseItem, LoadingSymbol, Plus, SearchBar, Welcome } from '../../../components'; +import { ExpenseItem, LoadingSymbol, Plus, TextInputBar, Welcome } from '../../../components'; import useFetch from '../../../hooks/useFetch'; import { addExpense, executeQuery } from "../../../services/database"; @@ -63,7 +63,7 @@ export default function Page() { } } - const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid;", args: []}); + const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid ORDER BY expense_datetime desc;", args: []}); const expenseDates = useMemo(()=> constructMarkedDates(data) @@ -92,7 +92,7 @@ export default function Page() { ListHeaderComponent={ <> {router.push("/home/userSettings")}}/> - - + } renderItem = {({item}) => } @@ -114,6 +114,9 @@ export default function Page() { ItemSeparatorComponent={() => { return (); }} + refreshControl={ + + } onScroll={handleScroll} scrollEventThrottle={20} /> @@ -124,6 +127,7 @@ export default function Page() { const styles = StyleSheet.create({ safeAreaViewStyle: { flex: 1, + paddingHorizontal: 10 }, itemSeperatorStyle: { marginVertical: 5, diff --git a/components/budget/budgetHeader.tsx b/components/budget/budgetHeader.tsx index eed9624..7fe8f01 100644 --- a/components/budget/budgetHeader.tsx +++ b/components/budget/budgetHeader.tsx @@ -1,5 +1,5 @@ import { StyleSheet, Text, TouchableHighlight, View } from "react-native"; -import SearchBar from "../common/SearchBar"; +import TextInputBar from "../common/TextInputBar"; import { useTheme } from "../../app/contexts/ThemeContext"; type BudgetHeaderProperties = { @@ -34,7 +34,7 @@ const BudgetHeader = (properties: BudgetHeaderProperties) => { }} /> - + ); } diff --git a/components/common/AutoDecimalInput.tsx b/components/common/AutoDecimalInput.tsx index e56aaa8..52821fc 100644 --- a/components/common/AutoDecimalInput.tsx +++ b/components/common/AutoDecimalInput.tsx @@ -1,6 +1,5 @@ import { View, Text, TouchableOpacity, TextInput, StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native' import React, {LegacyRef, MutableRefObject, useRef, useState} from 'react' -import colors from '../../constants/colors'; import { SIZES } from '../../constants/theme'; import { useTheme } from '../../app/contexts/ThemeContext'; @@ -69,17 +68,15 @@ const AutoDecimalInput: React.FC = ({onValueChange, label } const styles = StyleSheet.create({ - container: { - margin: SIZES.normal, - }, inputContainer: { + minHeight: 50, borderRadius: 20, flexDirection: "row", - justifyContent: 'space-between' + justifyContent: 'space-between', + alignItems: "center" }, text:{ fontSize: SIZES.large, - marginVertical: 12, marginHorizontal: 15, }, currency: { diff --git a/components/common/CategoryListItem.tsx b/components/common/CategoryListItem.tsx new file mode 100644 index 0000000..3d432a6 --- /dev/null +++ b/components/common/CategoryListItem.tsx @@ -0,0 +1,66 @@ +import { StyleSheet, Text, View, Pressable } from 'react-native' +import React from 'react' +import { Category } from '../../types/dbItems' +import CustomCard from './CustomCard'; +import { SIZES } from '../../constants/theme'; +import { useTheme } from '../../app/contexts/ThemeContext'; + +interface CategoryListItemProps{ + category: Category; + onPress?: (category: Category) =>void | undefined +} + + + +const CategoryListItem: React.FC = (props: CategoryListItemProps) => { + const {category, onPress} = props; + const {colors} = useTheme(); + + const handlePress = ()=>{ + if(onPress){ + onPress(category); + } + } + + return ( + + + + + {category.name ?? "#noData#"} + + + + + ) +} + +export default CategoryListItem + +const styles = StyleSheet.create({ + tile: { + height: 60, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + borderRadius: 20 + }, + colorTip:{ + height: "100%", + width: 30, + borderTopLeftRadius:20, + borderBottomLeftRadius: 20 + }, + textWrapper: { + + }, + tileTail:{ + height: "100%", + width: 30, + borderTopRightRadius:20, + borderBottomRightRadius: 20 + }, + text: { + fontSize: SIZES.large, + } +}) \ No newline at end of file diff --git a/components/common/CustomCard.tsx b/components/common/CustomCard.tsx index 5358d51..a866d8c 100644 --- a/components/common/CustomCard.tsx +++ b/components/common/CustomCard.tsx @@ -39,10 +39,7 @@ export default function CustomCard(props : ViewProps) { const styles = StyleSheet.create({ container:{ - flexDirection: "row", - alignItems: "stretch", - alignContent: "space-between", - borderRadius: 10, + borderRadius: 20, marginHorizontal: 10, }, boxShadow: {}, diff --git a/components/common/RoundedButton.tsx b/components/common/RoundedButton.tsx new file mode 100644 index 0000000..1852212 --- /dev/null +++ b/components/common/RoundedButton.tsx @@ -0,0 +1,32 @@ +import { StyleSheet, Text, View, ViewProps, TouchableOpacity } from 'react-native' +import React from 'react' +import { SIZES } from '../../constants/theme'; + +interface RoundedButtonProps extends ViewProps{ + onPress?: ()=> void | undefined; + color: string; +} + +const RoundedButton: React.FC = (props: RoundedButtonProps) => { + const {onPress, color, style, ...restProps} = props; + return ( + + + {restProps.children} + + + ) +} + +export default RoundedButton + +const styles = StyleSheet.create({ + btn:{ + justifyContent: 'center', + alignItems: 'center', + borderRadius: 80 + }, + text: { + fontSize: SIZES.normal + } +}) \ No newline at end of file diff --git a/components/common/TextInputBar.tsx b/components/common/TextInputBar.tsx index c276b68..a2ffa84 100644 --- a/components/common/TextInputBar.tsx +++ b/components/common/TextInputBar.tsx @@ -42,12 +42,12 @@ export default function TextInputBar(props: SearchBarProps) { //TODO: Handle textCancel // changed styles.container to containerStyle return ( - - + + handleChange(text)} onEndEditing={()=>setIsactive(false)}/> {isActive && {handleChange("")}}> - + } @@ -56,8 +56,6 @@ export default function TextInputBar(props: SearchBarProps) { const styles = StyleSheet.create({ container: { - marginHorizontal: 10, - marginBottom: 20, flexDirection: 'row', justifyContent: "center", alignItems: "center", diff --git a/components/home/addItem/CategorySelector.tsx b/components/home/addItem/CategorySelector.tsx new file mode 100644 index 0000000..27d5cfe --- /dev/null +++ b/components/home/addItem/CategorySelector.tsx @@ -0,0 +1,60 @@ +import { StyleSheet, Text, View, TouchableOpacity } from 'react-native' +import React, { useState } from 'react' +import { useTheme } from '../../../app/contexts/ThemeContext' +import { SIZES } from '../../../constants/theme'; +import { Category } from '../../../types/dbItems'; +import CategorySelectorModal from './CategorySelectorModal'; + +interface CategorySelectorProps { + onPress? : () => void | undefined; + selectedCategory? : Category; +} + +const CategorySelector: React.FC = (props : CategorySelectorProps) => { + const {colors} = useTheme(); + + + return ( + <> + + + + + + {props.selectedCategory?.name ?? "Tap to select Categroy"} + + + + + ) +} + +export default CategorySelector + +const styles = StyleSheet.create({ + tile: { + height: 60, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + borderRadius: 20 + }, + colorTip:{ + height: "100%", + width: 30, + borderTopLeftRadius:20, + borderBottomLeftRadius: 20 + }, + textWrapper: { + + }, + tileTail:{ + height: "100%", + width: 30, + borderTopRightRadius:20, + borderBottomRightRadius: 20 + }, + text: { + fontSize: SIZES.large, + } +}) \ No newline at end of file diff --git a/components/home/addItem/CategorySelectorModal.tsx b/components/home/addItem/CategorySelectorModal.tsx new file mode 100644 index 0000000..fb61c10 --- /dev/null +++ b/components/home/addItem/CategorySelectorModal.tsx @@ -0,0 +1,93 @@ +import { Modal, NativeSyntheticEvent, StyleSheet, Text, View, FlatList } from 'react-native' +import React, { useEffect, useMemo, useState } from 'react' +import { Category } from '../../../types/dbItems'; +import { useTheme } from '../../../app/contexts/ThemeContext'; +import CategoryListItem from '../../common/CategoryListItem'; +import { SIZES } from '../../../constants/theme'; +import useFetch from '../../../hooks/useFetch'; +import TextInputBar from '../../common/TextInputBar'; + + +interface CategorySelectorModalProps{ + visible: boolean; + onCategoryTap?: (category : Category) => void | undefined; + selectMulitple?: boolean; + onRequestClose?: ((event: NativeSyntheticEvent) => void) | undefined; +} + +//TODO: select Multiple + +const CategorySelectorModal: React.FC = (props : CategorySelectorModalProps) => { + const {visible, onCategoryTap, selectMulitple} = props; + const {data, reFetch} = useFetch({sql: "SELECT * FROM category;", args:[]}); + const {colors} = useTheme(); + const [searchtext, setSearchtext] = useState(""); + + const handleSearchText = (text : string) => { + setSearchtext(text); + } + + const categories = useMemo(()=>{ + return data.map((elem) => { + return {name: elem["name"], color: elem["color"], guid: elem["guid"]} + }) + }, [data]) + + const filteredCategories = categories.filter((category) => category.name?.toLowerCase().includes(searchtext.toLowerCase())) + + useEffect(()=>{ + if(visible){ + //reFetch(); Uncomment if newly added categories do not appear + handleSearchText(""); + } + }, [visible]) + + return ( + + + + + {selectMulitple ? "Categories" : "Category"} + + + item.guid!} + renderItem={({item})=> } + ItemSeparatorComponent={() => } + ListFooterComponent={() => } + keyboardShouldPersistTaps="always" + > + + + + + + ) +} + +export default CategorySelectorModal + +const styles = StyleSheet.create({ + main: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + height: "100%", + width: "100%", + alignItems: "center", + }, + modal: { + height: "70%", + width: "90%", + top: "10%", + borderRadius: 30, + paddingVertical: 20, + paddingHorizontal: 20 + }, + heading: { + fontSize: SIZES.xlarge, + fontWeight: "bold" + }, + itemSeperatorStyle: { + paddingBottom: 10 + } +}) \ No newline at end of file diff --git a/components/home/addItem/DateSelectorButton.tsx b/components/home/addItem/DateSelectorButton.tsx new file mode 100644 index 0000000..4f07297 --- /dev/null +++ b/components/home/addItem/DateSelectorButton.tsx @@ -0,0 +1,38 @@ +import { View, Text, StyleSheet, TouchableOpacity, TouchableOpacityProps } from 'react-native' +import React, { useState } from 'react' +import { useTheme } from '../../../app/contexts/ThemeContext'; +import { SIZES } from '../../../constants/theme'; +import { SimpleDate } from '../../../util/SimpleDate'; +import { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'; + +interface DateSelectorProps extends ViewProps { + onPress?: ()=>void | undefined; + selectedDate: Date +} + +const DateSelectorButton:React.FC = (props: DateSelectorProps) => { + const {onPress, selectedDate, ...restProps} = props; + const {colors} = useTheme(); + + return ( + + Date: + {new SimpleDate(selectedDate).format("DD.MM.YYYY")} + + ) +} +const styles = StyleSheet.create({ + inputContainer: { + minHeight: 50, + borderRadius: 20, + flexDirection: "row", + justifyContent: 'space-between', + alignItems: "center" + }, + text:{ + fontSize: SIZES.large, + marginHorizontal: 15, + }, +}) + +export default DateSelectorButton \ No newline at end of file diff --git a/components/home/expenseItem.tsx b/components/home/expenseItem.tsx index d541d48..ade5ccd 100644 --- a/components/home/expenseItem.tsx +++ b/components/home/expenseItem.tsx @@ -9,28 +9,29 @@ export default function ExpenseItem(itemProps : ExpenseItemProps) { const { colors } = useTheme(); return ( - - - {itemProps.category} - {itemProps.title} - {itemProps.date} - - - {itemProps.value} - - + + + + {itemProps.category} + {itemProps.title} + {itemProps.date} + + + {itemProps.value} + + ) @@ -42,6 +43,12 @@ const styles = StyleSheet.create({ borderTopLeftRadius: 20, borderBottomLeftRadius: 20, }, + + tile: { + flexDirection: "row", + alignItems: "stretch", + alignContent: "space-between", + }, textSection: { flexDirection: "column", diff --git a/components/index.tsx b/components/index.tsx index 56dc86c..b20c5fd 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -3,6 +3,11 @@ import { ButtonSetting, ToggleSetting } from "./home/Setting" import Welcome from "./home/Welcome" import ExpenseItem from "./home/expenseItem" +//home/addItem +import CategorySelector from "./home/addItem/CategorySelector" +import CategorySelectorModal from "./home/addItem/CategorySelectorModal" +import DateSelectorButton from "./home/addItem/DateSelectorButton" + //common import CustomCard from "./common/CustomCard" import NavigationButton from "./common/button" @@ -10,8 +15,7 @@ import LoadingSymbol from "./common/loadingSymbol" import Plus from "./common/plus" import TextInputBar from "./common/TextInputBar" import AutoDecimalInput from "./common/AutoDecimalInput" -import CategorySelector from "./common/CategorySelector" -import CategorySelectorModal from "./common/CategorySelectorModal" +import RoundedButton from "./common/RoundedButton" //login import BudgetHeader from "./budget/budgetHeader" @@ -23,8 +27,24 @@ import TypeSelectorSwitch from "./budget/typeSelectorSwitch" import Input from "./login/input" export { - BudgetHeader, ButtonSetting, CustomCard, ExpenseItem, Input, - LoadingSymbol, Plus, - TextInputBar, ToggleSetting, Welcome, AutoDecimalInput + BudgetHeader, + ButtonSetting, + CustomCard, + ExpenseItem, + Input, + LoadingSymbol, + Plus, + TextInputBar, + ToggleSetting, + Welcome, + AutoDecimalInput, + CategorySelector, + CategorySelectorModal, + DateSelectorButton, + RoundedButton, + CategoryItem, + TypeSelectorSwitch, + NavigationButton, + CustomColorPicker } diff --git a/package-lock.json b/package-lock.json index 16b6032..aab706f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/vector-icons": "^13.0.0", "@react-native-async-storage/async-storage": "1.18.2", + "@react-native-community/datetimepicker": "7.2.0", "@react-navigation/native": "^6.0.2", "expo": "~49.0.15", "expo-font": "~11.4.0", @@ -6275,6 +6276,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/@react-native-community/datetimepicker": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-7.2.0.tgz", + "integrity": "sha512-dO1sQy83M/EvnHE2egto05iwXZX7EYn5f/VDMp6afZFRFXRiRo7CzB3VFg4B55gJRJMNBv06NYMLPM3SlpnEGQ==", + "dependencies": { + "invariant": "^2.2.4" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.72.0", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", diff --git a/package.json b/package.json index abb4142..2e627b2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "react-native-screens": "~3.22.0", "react-native-uuid": "^2.0.1", "react-native-web": "~0.19.6", - "reanimated-color-picker": "^2.4.2" + "reanimated-color-picker": "^2.4.2", + "@react-native-community/datetimepicker": "7.2.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/services/DebugMenu.tsx b/services/DebugMenu.tsx index 896a667..29ce4e8 100644 --- a/services/DebugMenu.tsx +++ b/services/DebugMenu.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { View, Button, Alert } from 'react-native'; -import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase } from './database'; +import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase, deleteDatabase } from './database'; import uuid from 'react-native-uuid'; const randomColors = ["red", "blue", "green", "purple", "yellow"]; @@ -19,6 +19,11 @@ const getRandomNumber = () => { const DebugMenu = () => { + const deleteDBFile = () => { + console.warn("Deleting DB. App Restart is required") + return deleteDatabase(); + } + const handleNukeDatabase = () => { return deleteExpenses(), deleteCategories() }; @@ -60,6 +65,7 @@ const DebugMenu = () => { return ( +