From 6735b2d27139d1179ae583d75a05a3fc38234d8f Mon Sep 17 00:00:00 2001 From: ryansoe Date: Fri, 10 Apr 2026 18:10:12 -0700 Subject: [PATCH 1/5] feat: home ui migration --- frontend/app/(tabs)/index.tsx | 209 +- frontend/components/Graphs/PieChart.tsx | 44 +- .../components/Home/QuickActionsSection.tsx | 37 + .../components/Home/WeeklySpendingSection.tsx | 65 + .../NewTransaction/NewTransactionButton.tsx | 178 +- frontend/package-lock.json | 7851 +++++++++++------ 6 files changed, 5663 insertions(+), 2721 deletions(-) create mode 100644 frontend/components/Home/QuickActionsSection.tsx create mode 100644 frontend/components/Home/WeeklySpendingSection.tsx 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/components/Graphs/PieChart.tsx b/frontend/components/Graphs/PieChart.tsx index 835a6df..44a7b42 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,20 @@ 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..c3e4a8d --- /dev/null +++ b/frontend/components/Home/QuickActionsSection.tsx @@ -0,0 +1,37 @@ +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..170e86e --- /dev/null +++ b/frontend/components/Home/WeeklySpendingSection.tsx @@ -0,0 +1,65 @@ +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..7a584b7 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, YStack } 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; + 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,90 @@ 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); } }} /> -