diff --git a/frontend/app/(tabs)/Goals.tsx b/frontend/app/(tabs)/Goals.tsx index dab84e0..a0454f7 100644 --- a/frontend/app/(tabs)/Goals.tsx +++ b/frontend/app/(tabs)/Goals.tsx @@ -142,7 +142,7 @@ export default function Goals() { target_date: target_date, }), }) - .then((res) => { + .then(() => { setGoals( Goals.map((goal) => goal.id === id diff --git a/frontend/app/(tabs)/History.tsx b/frontend/app/(tabs)/History.tsx index 21668ec..f131012 100644 --- a/frontend/app/(tabs)/History.tsx +++ b/frontend/app/(tabs)/History.tsx @@ -3,7 +3,7 @@ import { Picker } from "@react-native-picker/picker"; import { useCallback, useState } from "react"; import BudgetChart from "@/components/HistoryBudget/BudgetChart"; import FullTransactionHistory from "@/components/TransactionHistory/FullTransactionHistory"; -import { StackRouter, useFocusEffect } from "@react-navigation/native"; +import { useFocusEffect } from "@react-navigation/native"; import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; import { ScrollView } from "react-native-gesture-handler"; diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 1a0fbb1..fd9b483 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -1,14 +1,18 @@ -import { View, StyleSheet, Text, ScrollView } from "react-native"; -import NewTransactionButton from "@/components/NewTransaction/NewTransactionButton"; -import TransactionHistory from "@/components/TransactionHistory/TransactionHistory"; -import { useEffect, useState, useCallback } from "react"; +import { ScrollView, XStack, YStack } from "tamagui"; +import { useState, useCallback } from "react"; import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; -import CustomPieChart from "@/components/Graphs/PieChart"; import { useFocusEffect } from "@react-navigation/native"; -/* - this function is the structure for the home screen which includes a graph, option to add transaction, and recent transaction history. -*/ +import { Screen } from "@/components/primitives/Screen"; +import { AppText } from "@/components/primitives/AppText"; +import { Card } from "@/components/primitives/Card"; +import { SectionTitle } from "@/components/primitives/SectionTitle"; +import { QuickActionsSection } from "@/components/Home/QuickActionsSection"; +import { WeeklySpendingSection } from "@/components/Home/WeeklySpendingSection"; +import NewTransactionButton from "@/components/NewTransaction/NewTransactionButton"; +import TransactionHistory from "@/components/TransactionHistory/TransactionHistory"; +import CustomPieChart from "@/components/Graphs/PieChart"; + interface Category { id: number; category_name: string; @@ -16,22 +20,32 @@ interface Category { max_category_budget: string; user_id: number; } + +interface Transaction { + id: number; + item_name: string; + amount: string; + category_name: string; + date: string; +} + +const categoryColors = new Map([ + ["Food", "#b8b8ff"], + ["Shopping", "#fff3b0"], + ["Transportation", "#588157"], + ["Subscriptions", "#ff9b85"], + ["Other", "#2b2d42"], +]); + export default function Home() { - //place holder array for us to map through - //passing it through props because I think it will be easier for us to call the API endpoints in the page and pass it through props const [ThreeTransactions, setThreeTransactions] = useState([]); + const [allTransactions, setAllTransactions] = useState([]); const [updateRecent, setUpdateRecent] = useState(false); const [total, setTotal] = useState(0); const [categories, setCategories] = useState([]); - const { userId } = useAuth(); const [username, setUsername] = useState(""); - const categoryColors = new Map([ - ["Food", "#b8b8ff"], // blue - ["Shopping", "#fff3b0"], //yellow - ["Transportation", "#588157"], //green - ["Subscriptions", "#ff9b85"], // red - ["Other", "#2b2d42"], //black - ]); + const [forceOpenTransaction, setForceOpenTransaction] = useState(false); + const { userId } = useAuth(); useFocusEffect( useCallback(() => { @@ -45,12 +59,10 @@ export default function Home() { }, }, ) - .then((res) => { - return res.json(); - }) + .then((res) => res.json()) .then((data) => { - console.log(data); setThreeTransactions(data.slice(0, 5)); + setAllTransactions(data); }) .catch((error) => { console.error("API Error:", error); @@ -59,9 +71,7 @@ export default function Home() { fetch(`http://localhost:${BACKEND_PORT}/users/${userId}`, { method: "GET", }) - .then((res) => { - return res.json(); - }) + .then((res) => res.json()) .then((data) => { setUsername(data.username); }) @@ -72,9 +82,7 @@ export default function Home() { fetch(`http://localhost:${BACKEND_PORT}/users/category/${userId}`, { method: "GET", }) - .then((res) => { - return res.json(); - }) + .then((res) => res.json()) .then((data) => { setCategories(data); setTotal( @@ -97,105 +105,56 @@ export default function Home() { name: category.category_name, id: category.id, })); - console.log(categories); + return ( - <> - - - - Hello {username} - - - Total Spending - - {/* */} - - - {pieData.map((category) => { - return ( - - - {category.name} - - ); - })} - - - {/* - components for the new transaction button and the list of transaction history. - */} - + + + + Hello {username} + + + + + + + {pieData.map((category) => ( + + + {category.name} + + ))} + + + + + + + + + setForceOpenTransaction(true)} /> + + + setForceOpenTransaction(false)} + /> + + + - - - - + + + + ); } - -const styles = StyleSheet.create({ - homeContainer: { - flex: 1, - alignItems: "center", - paddingVertical: 10, - paddingHorizontal: 20, - flexDirection: "column", - gap: 17, - }, - Title: { - fontWeight: "bold", - fontSize: 30, - width: "100%", - color: "#FFFFFF", - paddingHorizontal: 10, - }, - graphContainer: { - height: 500, - width: "100%", - backgroundColor: "white", - borderRadius: 15, - padding: 20, - flexDirection: "column", - justifyContent: "space-between", - gap: 30, - shadowRadius: 12, - shadowOpacity: 0.4, - }, - graph: { - width: "100%", - height: 180, - backgroundColor: "#E6E6E6", - borderRadius: 15, - }, - legendContainer: { - flexDirection: "row", - marginTop: 20, - flexWrap: "wrap", - gap: 10, - alignItems: "flex-start", - justifyContent: "flex-start", - width: "100%", - }, - legendItem: { - flexDirection: "row", - alignItems: "center", - gap: 10, - }, - colorBox: { - width: 20, - height: 20, - borderRadius: 4, - }, - legendText: { - fontSize: 16, - color: "black", - }, -}); diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index 7bfffc1..e4cee45 100644 --- a/frontend/app/_layout.tsx +++ b/frontend/app/_layout.tsx @@ -15,8 +15,8 @@ import { useAuth } from "@/context/authContext"; import { useRouter } from "expo-router"; import { ActivityIndicator, View } from "react-native"; import { BACKEND_PORT } from "@env"; -import { TamaguiProvider, Theme } from 'tamagui'; -import tamaguiConfig from '../tamagui.config'; +import { TamaguiProvider, Theme } from "tamagui"; +import tamaguiConfig from "../tamagui.config"; SplashScreen.preventAutoHideAsync(); @@ -33,7 +33,7 @@ function AuthCheck() { `http://localhost:${BACKEND_PORT}/auth/me`, { credentials: "include", - }, + } ); if (!response.ok) { diff --git a/frontend/app/demo.tsx b/frontend/app/demo.tsx index 397d3f6..5385693 100644 --- a/frontend/app/demo.tsx +++ b/frontend/app/demo.tsx @@ -1,74 +1,89 @@ -import React from 'react'; -import { ScrollView } from 'react-native'; -import { Screen } from '../components/primitives/Screen'; -import { AppText } from '../components/primitives/AppText'; -import { AppButton } from '../components/primitives/AppButton'; -import { StatCard } from '../components/primitives/StatCard'; -import { DonutCard } from '../components/primitives/DonutCard'; -import { QuickActionCard } from '../components/primitives/QuickActionCard'; -import { SegmentedControl } from '../components/primitives/SegmentedControl'; -import { YStack, XStack, Circle } from 'tamagui'; -import { Ionicons } from '@expo/vector-icons'; +import React from "react"; +import { ScrollView } from "react-native"; +import { Screen } from "../components/primitives/Screen"; +import { AppText } from "../components/primitives/AppText"; +import { AppButton } from "../components/primitives/AppButton"; +import { StatCard } from "../components/primitives/StatCard"; +import { DonutCard } from "../components/primitives/DonutCard"; +import { QuickActionCard } from "../components/primitives/QuickActionCard"; +import { SegmentedControl } from "../components/primitives/SegmentedControl"; +import { YStack, XStack, Circle } from "tamagui"; +import { Ionicons } from "@expo/vector-icons"; export default function DemoScreen() { return ( - {/* Header */} - + - + - JD + + JD + - Welcome Back, - Jordan 👋 + + Welcome Back, + + + Jordan 👋 + {/* Stats Row */} - } + } /> - } + } /> - + {/* Weekly Spending */} {/* Quick Actions */} - Quick Actions + + Quick Actions + - 💰} - backgroundColor="$surfaceTintYellow" + 💰} + backgroundColor="$surfaceTintYellow" /> - 📄} - backgroundColor="$surfaceTintGreen" + 📄} + backgroundColor="$surfaceTintGreen" /> - 🎯} + 🎯} backgroundColor="$surfaceTintGreen" // using green as fallback for the third from design /> @@ -83,10 +98,11 @@ export default function DemoScreen() { {/* Segmented Control Demo */} - Segmented Control Example + + Segmented Control Example + - diff --git a/frontend/components/GoalsList/GoalsList.tsx b/frontend/components/GoalsList/GoalsList.tsx index 98d3d71..56fed98 100644 --- a/frontend/components/GoalsList/GoalsList.tsx +++ b/frontend/components/GoalsList/GoalsList.tsx @@ -1,8 +1,4 @@ -import { useState } from "react"; -import { View, StyleSheet, Text } from "react-native"; -import { TextInput } from "react-native-gesture-handler"; -import { AntDesign } from "@expo/vector-icons"; -import SearchBar from "@/components/SearchBar/SearchBar"; +import { View, StyleSheet } from "react-native"; import GoalsRow from "./GoalsRow"; export default function GoalsList(props: any) { diff --git a/frontend/components/GoalsList/GoalsRow.tsx b/frontend/components/GoalsList/GoalsRow.tsx index 07a4b75..81ee4c5 100644 --- a/frontend/components/GoalsList/GoalsRow.tsx +++ b/frontend/components/GoalsList/GoalsRow.tsx @@ -6,7 +6,6 @@ import { Text, Animated, Pressable, - Button, TextInput, TouchableOpacity, Modal, diff --git a/frontend/components/Graphs/PieChart.tsx b/frontend/components/Graphs/PieChart.tsx index 835a6df..d9aec93 100644 --- a/frontend/components/Graphs/PieChart.tsx +++ b/frontend/components/Graphs/PieChart.tsx @@ -1,6 +1,7 @@ -import React from "react"; -import { ColorValue, View, StyleSheet } from "react-native"; -import Svg, { Path, G, Text } from "react-native-svg"; +import { ColorValue } from "react-native"; +import Svg, { Path, G } from "react-native-svg"; +import { YStack } from "tamagui"; +import { AppText } from "@/components/primitives/AppText"; export default function DoughnutChart(props: { total: number; @@ -11,7 +12,7 @@ export default function DoughnutChart(props: { const innerRadius = radius * 0.65; const total = props.data.reduce( (acc: any, item: { value: any }) => acc + item.value, - 0, + 0 ); let startAngle = 0; @@ -35,37 +36,29 @@ export default function DoughnutChart(props: { ].join(" "); startAngle = endAngle; - const textX = radius + radius * 0.5 * Math.sin(startAngle + angle / 2); // Mid-point of the arc - const textY = radius - radius * 0.5 * Math.cos(startAngle + angle / 2); // Mid-point of the arc return ; } return ( - - - {props.data.map((item) => createArc(item.value, item.color))} - + + + {props.data.map((item) => createArc(item.value, item.color))} + + ${props.total.toFixed(2)} - - - + + + ); } - -const styles = StyleSheet.create({ - PieContainer: { - justifyContent: "flex-start", - width: "100%", - alignItems: "center", - }, -}); diff --git a/frontend/components/Home/QuickActionsSection.tsx b/frontend/components/Home/QuickActionsSection.tsx new file mode 100644 index 0000000..b7c7204 --- /dev/null +++ b/frontend/components/Home/QuickActionsSection.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { XStack, YStack } from "tamagui"; +import { MaterialIcons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import { QuickActionCard } from "@/components/primitives/QuickActionCard"; +import { SectionTitle } from "@/components/primitives/SectionTitle"; + +interface QuickActionsSectionProps { + onAddExpense: () => void; +} + +export const QuickActionsSection: React.FC = ({ + onAddExpense, +}) => { + return ( + + + + + } + label="Add Expense" + onPress={onAddExpense} + /> + } + label="History" + onPress={() => router.push("/(tabs)/History")} + /> + } + label="Goals" + onPress={() => router.push("/(tabs)/Goals")} + /> + + + ); +}; diff --git a/frontend/components/Home/WeeklySpendingSection.tsx b/frontend/components/Home/WeeklySpendingSection.tsx new file mode 100644 index 0000000..90099f8 --- /dev/null +++ b/frontend/components/Home/WeeklySpendingSection.tsx @@ -0,0 +1,69 @@ +import React, { useState, useMemo } from "react"; +import { YStack } from "tamagui"; +import { SectionTitle } from "@/components/primitives/SectionTitle"; +import { SegmentedControl } from "@/components/primitives/SegmentedControl"; +import { StatCard } from "@/components/primitives/StatCard"; + +type Period = "1D" | "1W" | "1M" | "1Y"; + +interface Transaction { + id: number; + item_name: string; + amount: string; + category_name: string; + date: string; +} + +interface WeeklySpendingSectionProps { + transactions: Transaction[]; +} + +const PERIOD_LABELS: Record = { + "1D": "today", + "1W": "this week", + "1M": "this month", + "1Y": "this year", +}; + +function getCutoff(period: Period): Date { + const now = new Date(); + switch (period) { + case "1D": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "1W": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "1M": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + case "1Y": + return new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + } +} + +export const WeeklySpendingSection: React.FC = ({ + transactions, +}) => { + const [period, setPeriod] = useState("1M"); + + const filteredTotal = useMemo(() => { + const cutoff = getCutoff(period); + return transactions + .filter((t) => new Date(t.date) >= cutoff) + .reduce((sum, t) => sum + parseFloat(t.amount), 0); + }, [transactions, period]); + + return ( + + + setPeriod(val)} + /> + + + ); +}; diff --git a/frontend/components/NewTransaction/NewTransactionButton.tsx b/frontend/components/NewTransaction/NewTransactionButton.tsx index 7989bd1..05f6f88 100644 --- a/frontend/components/NewTransaction/NewTransactionButton.tsx +++ b/frontend/components/NewTransaction/NewTransactionButton.tsx @@ -1,63 +1,77 @@ -import { - View, - StyleSheet, - Text, - Animated, - Pressable, - TextInput, - Button, -} from "react-native"; +import { Animated, Pressable } from "react-native"; import { MaterialIcons } from "@expo/vector-icons"; -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Picker } from "@react-native-picker/picker"; import Toast from "react-native-toast-message"; import { BACKEND_PORT } from "@env"; import { useAuth } from "@/context/authContext"; +import { XStack } from "tamagui"; +import { AppText } from "@/components/primitives/AppText"; +import { AppInput } from "@/components/primitives/AppInput"; +import { AppButton } from "@/components/primitives/AppButton"; -//button that expands and shows a text input for recent transactions -export default function NewTransactionButton(props: any) { - //usestate for expanding button, if true, button is expanded, initially set to false +interface NewTransactionButtonProps { + setUpdateRecent: (_val: boolean) => void; // eslint-disable-line no-unused-vars + updateRecent: boolean; + forceOpen?: boolean; + onForceOpenHandled?: () => void; +} + +export default function NewTransactionButton({ + setUpdateRecent, + updateRecent, + forceOpen, + onForceOpenHandled, +}: NewTransactionButtonProps) { const [inputVisible, setInputVisible] = useState(false); - //refs for animation(X icon rotating, button expanding... etc) const rotation = useRef(new Animated.Value(0)).current; const inputShow = useRef(new Animated.Value(-20)).current; const expand = useRef(new Animated.Value(50)).current; const animatedOpacity = useRef(new Animated.Value(0)).current; const { userId } = useAuth(); - //rotation iterpolate for X icon + const interpolate = rotation.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "45deg"], }); - //for the selected category and the transaction amount const [selectedCategory, setSelectedCategory] = useState(""); const [transactionAmount, setTransactionAmount] = useState(""); const [itemInformation, setItemInformation] = useState(""); - //toggle function for buttion, starts all animation when toggled and sets inputVisible accordingly - function toggle() { - setInputVisible(!inputVisible); + + function toggle(forceExpand?: boolean) { + const shouldOpen = forceExpand !== undefined ? forceExpand : !inputVisible; + if (shouldOpen === inputVisible) return; + setInputVisible(shouldOpen); Animated.spring(rotation, { - toValue: inputVisible ? 0 : 1, + toValue: shouldOpen ? 1 : 0, tension: 100, useNativeDriver: true, }).start(); Animated.spring(expand, { - toValue: inputVisible ? 50 : 260, + toValue: shouldOpen ? 260 : 50, tension: 40, useNativeDriver: false, }).start(); Animated.timing(inputShow, { - toValue: inputVisible ? -20 : 0, + toValue: shouldOpen ? 0 : -20, duration: 300, useNativeDriver: true, }).start(); Animated.timing(animatedOpacity, { - toValue: inputVisible ? 0 : 1, + toValue: shouldOpen ? 1 : 0, duration: 300, useNativeDriver: true, }).start(); } + + useEffect(() => { + if (forceOpen) { + toggle(true); + onForceOpenHandled?.(); + } + }, [forceOpen]); + function addTransaction() { fetch(`http://localhost:${BACKEND_PORT}/transactions/newTransaction`, { method: "POST", @@ -80,11 +94,10 @@ export default function NewTransactionButton(props: any) { text1: "Transaction Unsuccessful ❌", text2: "One or more fields are invalid, try again", }); - throw new Error(err.error || "Something went wrong"); }); } - props.setUpdateRecent(!props.updateRecent); + setUpdateRecent(!updateRecent); setItemInformation(""); setTransactionAmount(""); setSelectedCategory(""); @@ -93,121 +106,92 @@ export default function NewTransactionButton(props: any) { text1: "Transaction Successful ✅", text2: "Your transaction has been recorded", }); - return res.json(); }) .catch((error) => { console.error("API Error:", error); }); } + return ( - - - - New Transaction + + toggle()} style={{ width: "100%" }}> + + + New Transaction + - + {inputVisible ? ( setSelectedCategory(itemValue)} - style={styles.picker} + style={{ + width: "100%", + borderWidth: 3, + borderRadius: 10, + padding: 10, + borderColor: "#E5E5E5", + backgroundColor: "#fff", + }} > - {/* <<<<<<< HEAD - - - - - -======= */} - {/* >>>>>>> main */} - setItemInformation(e)} value={itemInformation} /> - { - // Allow only numeric values if (/^\d*\.?\d*$/.test(text)) { setTransactionAmount(text); } }} /> -