From dbacc2468a25b13fbed2408450ed63ed7095c2c7 Mon Sep 17 00:00:00 2001 From: Nhat Tran Date: Sat, 11 Apr 2026 17:22:40 -0700 Subject: [PATCH 1/2] Lint fix --- frontend/app/_layout.tsx | 4 +- frontend/app/demo.tsx | 104 ++++++++++-------- frontend/components/primitives/AppButton.tsx | 32 +++--- frontend/components/primitives/AppInput.tsx | 36 +++--- frontend/components/primitives/AppText.tsx | 22 ++-- frontend/components/primitives/Card.tsx | 16 +-- frontend/components/primitives/DonutCard.tsx | 34 ++++-- frontend/components/primitives/GlassCard.tsx | 14 +-- .../components/primitives/QuickActionCard.tsx | 32 ++++-- frontend/components/primitives/Screen.tsx | 4 +- .../components/primitives/SectionTitle.tsx | 28 +++-- .../primitives/SegmentedControl.tsx | 26 ++--- frontend/components/primitives/StatCard.tsx | 39 ++++--- frontend/constants/tamagui-tokens.ts | 58 +++++----- frontend/tamagui.config.ts | 22 ++-- 15 files changed, 267 insertions(+), 204 deletions(-) diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index 7bfffc1..db7bfee 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(); 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/primitives/AppButton.tsx b/frontend/components/primitives/AppButton.tsx index 3819c23..47208a1 100644 --- a/frontend/components/primitives/AppButton.tsx +++ b/frontend/components/primitives/AppButton.tsx @@ -1,13 +1,13 @@ -import { Button, styled } from 'tamagui'; +import { Button, styled } from "tamagui"; export const AppButton = styled(Button, { - backgroundColor: '$primary', - color: 'white', - borderRadius: '$7', // Pill shape - paddingHorizontal: '$5', - paddingVertical: '$3', - alignItems: 'center', - justifyContent: 'center', + backgroundColor: "$primary", + color: "white", + borderRadius: "$7", // Pill shape + paddingHorizontal: "$5", + paddingVertical: "$3", + alignItems: "center", + justifyContent: "center", borderWidth: 0, hoverStyle: { @@ -20,22 +20,22 @@ export const AppButton = styled(Button, { variants: { variant: { primary: { - backgroundColor: '$primary', - color: 'white', + backgroundColor: "$primary", + color: "white", }, secondary: { - backgroundColor: '$secondary', - color: 'white', + backgroundColor: "$secondary", + color: "white", }, outline: { - backgroundColor: 'transparent', + backgroundColor: "transparent", borderWidth: 1, - borderColor: '$primary', - color: '$primary', + borderColor: "$primary", + color: "$primary", }, }, } as const, defaultVariants: { - variant: 'primary', + variant: "primary", }, }); diff --git a/frontend/components/primitives/AppInput.tsx b/frontend/components/primitives/AppInput.tsx index bbe0f47..7333b1c 100644 --- a/frontend/components/primitives/AppInput.tsx +++ b/frontend/components/primitives/AppInput.tsx @@ -1,17 +1,17 @@ -import { Input, styled } from 'tamagui'; +import { Input, styled } from "tamagui"; export const AppInput = styled(Input, { - backgroundColor: '$surface', - borderColor: '$border', + backgroundColor: "$surface", + borderColor: "$border", borderWidth: 1, - borderRadius: '$3', - paddingHorizontal: '$3', - paddingVertical: '$3', - color: '$text', - fontFamily: '$body', + borderRadius: "$3", + paddingHorizontal: "$3", + paddingVertical: "$3", + color: "$text", + fontFamily: "$body", focusStyle: { - borderColor: '$primary', + borderColor: "$primary", borderWidth: 2, }, @@ -19,27 +19,27 @@ export const AppInput = styled(Input, { size: { small: { height: 36, - fontSize: '$2', + fontSize: "$2", }, medium: { height: 48, - fontSize: '$3', + fontSize: "$3", }, large: { height: 56, - fontSize: '$4', + fontSize: "$4", }, }, error: { true: { - borderColor: '$danger', + borderColor: "$danger", focusStyle: { - borderColor: '$danger', - } - } - } + borderColor: "$danger", + }, + }, + }, } as const, defaultVariants: { - size: 'medium', + size: "medium", }, }); diff --git a/frontend/components/primitives/AppText.tsx b/frontend/components/primitives/AppText.tsx index 989b0e0..38ecba1 100644 --- a/frontend/components/primitives/AppText.tsx +++ b/frontend/components/primitives/AppText.tsx @@ -1,29 +1,29 @@ -import { Text, styled } from 'tamagui'; +import { Text, styled } from "tamagui"; export const AppText = styled(Text, { - color: '$color', - fontFamily: '$body', + color: "$color", + fontFamily: "$body", variants: { variant: { title: { - fontSize: '$7', - fontWeight: 'bold', + fontSize: "$7", + fontWeight: "bold", }, subtitle: { - fontSize: '$5', - color: '$textMuted', + fontSize: "$5", + color: "$textMuted", }, body: { - fontSize: '$3', + fontSize: "$3", }, caption: { - fontSize: '$2', - color: '$textMuted', + fontSize: "$2", + color: "$textMuted", }, }, } as const, defaultVariants: { - variant: 'body', + variant: "body", }, }); diff --git a/frontend/components/primitives/Card.tsx b/frontend/components/primitives/Card.tsx index 7501b7d..52b3002 100644 --- a/frontend/components/primitives/Card.tsx +++ b/frontend/components/primitives/Card.tsx @@ -1,16 +1,16 @@ -import { YStack, styled } from 'tamagui'; +import { YStack, styled } from "tamagui"; export const Card = styled(YStack, { - backgroundColor: '$surfaceDefault', - borderRadius: '$5', // 24px radius - padding: '$4', - - // By default cards are flat in the new design. + backgroundColor: "$surfaceDefault", + borderRadius: "$5", // 24px radius + padding: "$4", + + // By default cards are flat in the new design. // We can override with elevated variant if needed. variants: { padded: { true: { - padding: '$5', + padding: "$5", }, }, elevated: { @@ -18,6 +18,6 @@ export const Card = styled(YStack, { shadowOpacity: 0.2, elevation: 6, }, - } + }, } as const, }); diff --git a/frontend/components/primitives/DonutCard.tsx b/frontend/components/primitives/DonutCard.tsx index 1095cde..6d9b502 100644 --- a/frontend/components/primitives/DonutCard.tsx +++ b/frontend/components/primitives/DonutCard.tsx @@ -1,21 +1,35 @@ -import React from 'react'; -import { Card } from './Card'; -import { AppText } from './AppText'; -import { YStack, Circle } from 'tamagui'; +import React from "react"; +import { Card } from "./Card"; +import { AppText } from "./AppText"; +import { YStack, Circle } from "tamagui"; // Placeholder for an actual Donut chart which would typically use react-native-svg // or a charting library -export const DonutCard: React.FC<{ title: string; centerMetric: string }> = ({ title, centerMetric }) => { +export const DonutCard: React.FC<{ title: string; centerMetric: string }> = ({ + title, + centerMetric, +}) => { return ( - {title} - + + {title} + + {/* Simple visual placeholder for a donut chart */} - + - {centerMetric} - of budget + + {centerMetric} + + + of budget + diff --git a/frontend/components/primitives/GlassCard.tsx b/frontend/components/primitives/GlassCard.tsx index b6b7c02..1f83f10 100644 --- a/frontend/components/primitives/GlassCard.tsx +++ b/frontend/components/primitives/GlassCard.tsx @@ -1,12 +1,12 @@ -import { styled } from 'tamagui'; -import { BlurView } from 'expo-blur'; +import { styled } from "tamagui"; +import { BlurView } from "expo-blur"; export const GlassCard = styled(BlurView, { - tint: 'default', + tint: "default", intensity: 50, - borderRadius: '$4', - padding: '$4', - borderColor: 'rgba(255, 255, 255, 0.3)', + borderRadius: "$4", + padding: "$4", + borderColor: "rgba(255, 255, 255, 0.3)", borderWidth: 1, - overflow: 'hidden', + overflow: "hidden", }); diff --git a/frontend/components/primitives/QuickActionCard.tsx b/frontend/components/primitives/QuickActionCard.tsx index 7eb2d18..809436d 100644 --- a/frontend/components/primitives/QuickActionCard.tsx +++ b/frontend/components/primitives/QuickActionCard.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Card } from './Card'; -import { AppText } from './AppText'; -import { YStack, GetProps } from 'tamagui'; +import React from "react"; +import { Card } from "./Card"; +import { AppText } from "./AppText"; +import { YStack, GetProps } from "tamagui"; type CardProps = GetProps; @@ -11,12 +11,17 @@ interface QuickActionCardProps extends CardProps { onPress?: () => void; } -export const QuickActionCard: React.FC = ({ icon, label, onPress, ...props }) => { +export const QuickActionCard: React.FC = ({ + icon, + label, + onPress, + ...props +}) => { return ( - = ({ icon, label, o > {icon} - {label} + + {label} + ); diff --git a/frontend/components/primitives/Screen.tsx b/frontend/components/primitives/Screen.tsx index 691bb7b..7604de5 100644 --- a/frontend/components/primitives/Screen.tsx +++ b/frontend/components/primitives/Screen.tsx @@ -1,6 +1,6 @@ -import { YStack, styled } from 'tamagui'; +import { YStack, styled } from "tamagui"; export const Screen = styled(YStack, { flex: 1, - backgroundColor: '$background', + backgroundColor: "$background", }); diff --git a/frontend/components/primitives/SectionTitle.tsx b/frontend/components/primitives/SectionTitle.tsx index dc50a1e..7100714 100644 --- a/frontend/components/primitives/SectionTitle.tsx +++ b/frontend/components/primitives/SectionTitle.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { XStack } from 'tamagui'; -import { AppText } from './AppText'; +import React from "react"; +import { XStack } from "tamagui"; +import { AppText } from "./AppText"; interface SectionTitleProps { title: string; @@ -8,14 +8,24 @@ interface SectionTitleProps { onAction?: () => void; } -export const SectionTitle: React.FC = ({ title, actionText, onAction }) => { +export const SectionTitle: React.FC = ({ + title, + actionText, + onAction, +}) => { return ( - - {title} + + + {title} + {actionText && ( - diff --git a/frontend/components/primitives/SegmentedControl.tsx b/frontend/components/primitives/SegmentedControl.tsx index 9b589a7..b3f2a0a 100644 --- a/frontend/components/primitives/SegmentedControl.tsx +++ b/frontend/components/primitives/SegmentedControl.tsx @@ -1,18 +1,18 @@ -import React, { useState } from 'react'; -import { XStack, YStack } from 'tamagui'; -import { AppText } from './AppText'; +import React, { useState } from "react"; +import { XStack, YStack } from "tamagui"; +import { AppText } from "./AppText"; -const PERIODS = ['1D', '1W', '1M', '1Y'] as const; -type Period = typeof PERIODS[number]; +const PERIODS = ["1D", "1W", "1M", "1Y"] as const; +type Period = (typeof PERIODS)[number]; interface SegmentedControlProps { onValueChange?: (value: Period) => void; defaultValue?: Period; } -export const SegmentedControl: React.FC = ({ - onValueChange, - defaultValue = '1M' +export const SegmentedControl: React.FC = ({ + onValueChange, + defaultValue = "1M", }) => { const [active, setActive] = useState(defaultValue); @@ -22,7 +22,7 @@ export const SegmentedControl: React.FC = ({ }; return ( - = ({ justifyContent="center" paddingVertical="$2" borderRadius="$7" // Pill shaped items - backgroundColor={isActive ? '$primary' : 'transparent'} + backgroundColor={isActive ? "$primary" : "transparent"} onPress={() => handlePress(period)} > - {period} diff --git a/frontend/components/primitives/StatCard.tsx b/frontend/components/primitives/StatCard.tsx index d05df9d..dca1d94 100644 --- a/frontend/components/primitives/StatCard.tsx +++ b/frontend/components/primitives/StatCard.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Card } from './Card'; -import { AppText } from './AppText'; -import { YStack, XStack, GetProps } from 'tamagui'; +import React from "react"; +import { Card } from "./Card"; +import { AppText } from "./AppText"; +import { YStack, XStack, GetProps } from "tamagui"; type CardProps = GetProps; @@ -9,29 +9,40 @@ interface StatCardProps extends CardProps { title: string; value: string; subtitle?: string; - trend?: 'up' | 'down' | 'neutral'; + trend?: "up" | "down" | "neutral"; icon?: React.ReactNode; } -export const StatCard: React.FC = ({ title, value, subtitle, trend, icon, ...props }) => { +export const StatCard: React.FC = ({ + title, + value, + subtitle, + trend, + icon, + ...props +}) => { return ( - - {title} + + {title} + {icon} - {value} + + {value} + {subtitle && ( - {subtitle} diff --git a/frontend/constants/tamagui-tokens.ts b/frontend/constants/tamagui-tokens.ts index 1bbc780..c54a449 100644 --- a/frontend/constants/tamagui-tokens.ts +++ b/frontend/constants/tamagui-tokens.ts @@ -1,4 +1,4 @@ -import { createTokens } from 'tamagui'; +import { createTokens } from "tamagui"; export const tokens = createTokens({ size: { @@ -39,40 +39,40 @@ export const tokens = createTokens({ }, color: { // Light Theme Colors - lightText: '#11181C', - lightBackground: '#ffffff', - lightTint: '#0a7ea4', - lightIcon: '#687076', - + lightText: "#11181C", + lightBackground: "#ffffff", + lightTint: "#0a7ea4", + lightIcon: "#687076", + // Dark Theme Colors - darkText: '#ECEDEE', - darkBackground: '#151718', - darkTint: '#ffffff', - darkIcon: '#9BA1A6', + darkText: "#ECEDEE", + darkBackground: "#151718", + darkTint: "#ffffff", + darkIcon: "#9BA1A6", // Generic Colors - white: '#FFFFFF', - black: '#000000', - primary: '#395773', // the navy blue from the design - secondary: '#5856D6', - + white: "#FFFFFF", + black: "#000000", + primary: "#395773", // the navy blue from the design + secondary: "#5856D6", + // Surface variants - surfaceDefault: '#FFFFFF', - surfaceTintBlue: '#E6F1F4', // daily spending / goals - surfaceTintGreen: '#EAEFE0', // weekly spending donut card - surfaceTintYellow: '#F6F3E6', // quick actions save more - darkSurfaceDefault: '#212325', - darkSurfaceTintBlue: '#0A2540', - darkSurfaceTintGreen: '#0D2916', + surfaceDefault: "#FFFFFF", + surfaceTintBlue: "#E6F1F4", // daily spending / goals + surfaceTintGreen: "#EAEFE0", // weekly spending donut card + surfaceTintYellow: "#F6F3E6", // quick actions save more + darkSurfaceDefault: "#212325", + darkSurfaceTintBlue: "#0A2540", + darkSurfaceTintGreen: "#0D2916", // Semantic - text: '#1C252E', - textMuted: '#7B8A96', - border: '#C6C6C8', - danger: '#FF3B30', - success: '#34C759', - warning: '#FFCC00', - info: '#5AC8FA', + text: "#1C252E", + textMuted: "#7B8A96", + border: "#C6C6C8", + danger: "#FF3B30", + success: "#34C759", + warning: "#FFCC00", + info: "#5AC8FA", }, radius: { 0: 0, diff --git a/frontend/tamagui.config.ts b/frontend/tamagui.config.ts index 9ff0352..111b8ac 100644 --- a/frontend/tamagui.config.ts +++ b/frontend/tamagui.config.ts @@ -1,8 +1,8 @@ -import { createTamagui, createFont } from 'tamagui'; -import { tokens } from './constants/tamagui-tokens'; +import { createTamagui, createFont } from "tamagui"; +import { tokens } from "./constants/tamagui-tokens"; const interFont = createFont({ - family: 'Inter, Helvetica, Arial, sans-serif', + family: "Inter, Helvetica, Arial, sans-serif", size: { 1: 12, 2: 14, @@ -27,8 +27,8 @@ const interFont = createFont({ 9: 60, }, weight: { - 4: '400', - 7: '700', + 4: "400", + 7: "700", }, letterSpacing: { 4: 0, @@ -44,7 +44,7 @@ export const tamaguiConfig = createTamagui({ }, themes: { light: { - background: '#F7F9FA', // soft off-white from design + background: "#F7F9FA", // soft off-white from design color: tokens.color.text, primary: tokens.color.primary, tint: tokens.color.primary, @@ -75,16 +75,16 @@ export const tamaguiConfig = createTamagui({ }, }, shorthands: { - px: 'paddingHorizontal', - py: 'paddingVertical', - mx: 'marginHorizontal', - my: 'marginVertical', + px: "paddingHorizontal", + py: "paddingVertical", + mx: "marginHorizontal", + my: "marginVertical", } as const, }); export type AppConfig = typeof tamaguiConfig; -declare module 'tamagui' { +declare module "tamagui" { interface TamaguiCustomConfig extends AppConfig {} } From 5eb972eecca8531daf07ef8471e70bd7471d8397 Mon Sep 17 00:00:00 2001 From: Nhat Tran Date: Fri, 1 May 2026 00:33:48 -0700 Subject: [PATCH 2/2] feat: new History page UI --- frontend/app/(tabs)/History.tsx | 610 ++++++------------ .../NewTransaction/NewTransactionButton.tsx | 8 - .../FullTransactionHistory.tsx | 114 +--- .../TransactionHistory/NewTransactionRow.tsx | 116 ++++ .../TransactionHistory/TransactionRow.tsx | 7 - frontend/components/primitives/AppSelect.tsx | 53 ++ frontend/components/primitives/AppSwitch.tsx | 23 + .../primitives/SegmentedControl.tsx | 23 +- frontend/constants/tamagui-tokens.ts | 1 + frontend/types/transaction.ts | 14 + 10 files changed, 448 insertions(+), 521 deletions(-) create mode 100644 frontend/components/TransactionHistory/NewTransactionRow.tsx create mode 100644 frontend/components/primitives/AppSelect.tsx create mode 100644 frontend/components/primitives/AppSwitch.tsx create mode 100644 frontend/types/transaction.ts diff --git a/frontend/app/(tabs)/History.tsx b/frontend/app/(tabs)/History.tsx index 21668ec..37a21bc 100644 --- a/frontend/app/(tabs)/History.tsx +++ b/frontend/app/(tabs)/History.tsx @@ -1,61 +1,99 @@ -import { View, StyleSheet, Text, TouchableOpacity } from "react-native"; -import { Picker } from "@react-native-picker/picker"; -import { useCallback, useState } from "react"; -import BudgetChart from "@/components/HistoryBudget/BudgetChart"; +import { TouchableOpacity } from "react-native"; +import { useCallback, useMemo, useState } from "react"; 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"; import CustomLineChart from "@/components/Graphs/LineChart"; import { useWindowDimensions } from "react-native"; +import { Screen } from "@/components/primitives/Screen"; +import { SegmentedControl } from "@/components/primitives/SegmentedControl"; +import Transaction, { Category } from "@/types/transaction"; +import { AppText } from "@/components/primitives/AppText"; +import { StatCard } from "@/components/primitives/StatCard"; +import { Ionicons } from "@expo/vector-icons"; +import { XStack, YStack } from "tamagui"; +import { AppSelect } from "@/components/primitives/AppSelect"; +import { AppSwitch } from "@/components/primitives/AppSwitch"; +import { AppButton } from "@/components/primitives/AppButton"; + +type SortOption = "Date" | "Amount" | "Name"; +type FilterOption = "All" | "Month" | "Category"; // Page for showing full Expense History along with the user's budget and how much they spent compared to their budget export default function History() { + // YYYY-MM format + const currentMonth = new Date().toISOString().substring(0, 7); + // Sorting State - const [sortBy, setSortBy] = useState("date"); - const [sortOrder, setSortOrder] = useState("desc"); // "asc" or "desc" - const [AllTransactions, setAllTransactions] = useState([]); + const [sortBy, setSortBy] = useState("Date"); + const [sortIsAscending, setSortIsAscending] = useState(false); + const [AllTransactions, setAllTransactions] = useState([]); const { userId } = useAuth(); - const [showSortOptions, setShowSortOptions] = useState(false); - const [budget, setBudget] = useState(0); // Filter State - const [filterType, setFilterType] = useState("none"); // "none", "month", "category" - const [selectedMonth, setSelectedMonth] = useState( - new Date().toISOString().substring(0, 7), - ); // YYYY-MM format + const [filterType, setFilterType] = useState("All"); + const [selectedMonth, setSelectedMonth] = useState(currentMonth); const [selectedCategory, setSelectedCategory] = useState("Food"); - const [showFilterOptions, setShowFilterOptions] = useState(false); const [lineChartData, setLineChartData] = useState([]); - const [selectedTimeRange, setSelectedTimeRange] = useState("3months"); + const [selectedTimeRange, setSelectedTimeRange] = useState("1M"); + + // Edit State + const [showSettings, setShowSettings] = useState(false); const screenWidth = useWindowDimensions().width; const chartWidth = screenWidth * 0.75; // Time range configuration const timeRangeConfig = { - "1month": { period: "daily", months: 1, label: "1 Month" }, - "3months": { period: "weekly", months: 3, label: "3 Months" }, - "6months": { period: "weekly", months: 6, label: "6 Months" }, - "1year": { period: "weekly", months: 12, label: "1 Year" }, + "1M": { period: "daily", months: 1, label: "1 Month" }, + "3M": { period: "weekly", months: 3, label: "3 Months" }, + "6M": { period: "weekly", months: 6, label: "6 Months" }, + "1Y": { period: "weekly", months: 12, label: "1 Year" }, }; - // Get unique categories from transactions - const getUniqueCategories = () => { - const uniqueCategories = [ - ...new Set(AllTransactions.map((trans) => trans.category_name)), - ].filter(Boolean); + const categoryIconMapping: Map< + Category, + | "fast-food-outline" + | "pricetag-outline" + | "bus-outline" + | "calendar-outline" + | "planet-outline" + > = new Map([ + ["Food", "fast-food-outline"], + ["Shopping", "pricetag-outline"], + ["Transportation", "bus-outline"], + ["Subscriptions", "calendar-outline"], + ["Other", "planet-outline"], + ]); + + const categories: Category[] = [ + "Food", + "Shopping", + "Subscriptions", + "Transportation", + "Other", + ]; - // If no categories found in transactions or they're unexpected, use default categories - if (uniqueCategories.length === 0) { - return ["Food", "Shopping", "Subscriptions", "Transportation", "Other"]; - } + // Get unique months from transactions + const months = useMemo(() => { + const months = [ + ...new Set( + AllTransactions.map((trans) => + new Date(trans.date).toISOString().substring(0, 7), + ), + ), + ] + .sort() + .reverse(); // Sort in descending order - return uniqueCategories; - }; + if (months.length == 0 || months[0] !== currentMonth) { + return [currentMonth, ...months]; + } - const categories = getUniqueCategories(); + return months; + }, [AllTransactions]); //our app only loads once and does not load again even if we change tabs. This is why we cant use useEffect //we use useFocusEffect to detect if our tab is in focus rather than using useEffect @@ -74,23 +112,12 @@ export default function History() { .then((res) => res.json()) .then((data) => { setAllTransactions(data); + console.log(data); }) .catch((error) => { console.error("API Error:", error); }); - fetch(`http://localhost:${BACKEND_PORT}/users/${userId}`, { - method: "GET", - }) - .then((res) => { - return res.json(); - }) - .then((data) => { - setBudget(data.total_budget); - }) - .catch((error) => { - console.error("API Error:", error); - }); const config = timeRangeConfig[selectedTimeRange as keyof typeof timeRangeConfig]; fetch( @@ -113,30 +140,29 @@ export default function History() { }, [selectedTimeRange]), ); - // Toggle filter options visibility - const toggleFilterOptions = () => { - setShowFilterOptions(!showFilterOptions); + const toggleSettings = () => { + setShowSettings(!showSettings); }; - // Reset filters - const resetFilters = () => { - setFilterType("none"); - setSelectedCategory("Food"); - setShowFilterOptions(false); + // Reset filters & sort + const resetFiltersAndSort = () => { + setFilterType("All"); + setSortBy("Date"); + setSortIsAscending(false); }; // Filtering Logic const filteredTransactions = AllTransactions.filter((transaction) => { - if (filterType === "none") return true; + if (filterType === "All") return true; - if (filterType === "month") { + if (filterType === "Month") { const transactionDate = new Date(transaction.date) .toISOString() .substring(0, 7); return transactionDate === selectedMonth; } - if (filterType === "category") { + if (filterType === "Category") { // Handle case where transaction might not have a category const transactionCategory = transaction.category_name || ""; return transactionCategory === selectedCategory; @@ -148,367 +174,153 @@ export default function History() { // Sorting Logic const sortedTransactions = [...filteredTransactions].sort((a, b) => { let result = 0; - if (sortBy === "date") { + if (sortBy === "Date") { result = new Date(a.date).getTime() - new Date(b.date).getTime(); - } else if (sortBy === "amount") { - result = a.amount - b.amount; - } else if (sortBy === "name") { + } else if (sortBy === "Amount") { + result = parseFloat(a.amount) - parseFloat(b.amount); + } else { result = a.item_name.localeCompare(b.item_name); } - return sortOrder === "asc" ? result : -result; + return sortIsAscending ? result : -result; }); // Calculate total from filtered transactions - const totalAmount = filteredTransactions.reduce((sum, transaction) => { - return sum + parseFloat(transaction.amount || 0); - }, 0); - - // Format month for display - const formatMonth = (dateString: string) => { - const [year, month] = dateString.split("-"); - const date = new Date(parseInt(year), parseInt(month) - 1); - return date.toLocaleString("default", { month: "long", year: "numeric" }); - }; - - // Get unique months from transactions - const getAvailableMonths = () => { - const months = [ - ...new Set( - AllTransactions.map((trans) => - new Date(trans.date).toISOString().substring(0, 7), - ), + const totalAmount = useMemo(() => { + return ( + filterType == "Month" ? filteredTransactions : AllTransactions + ).reduce((sum, transaction) => sum + parseFloat(transaction.amount), 0); + }, [filterType, filteredTransactions]); + + // Calculate total for each category, in descending order + const categoryTotals = useMemo(() => { + const initialTotals = categories.reduce( + (map, category) => map.set(category, 0), + new Map(), + ); + const totals = [ + ...(filterType == "Month" + ? filteredTransactions + : AllTransactions + ).reduce( + (currentTotals, transaction) => + currentTotals.set( + transaction.category_name, + (currentTotals.get(transaction.category_name) as number) + + parseFloat(transaction.amount), + ), + initialTotals, ), - ] - .sort() - .reverse(); // Sort in descending order + ]; + totals.sort((a, b) => b[1] - a[1]); - return months; - }; + return totals.filter(([_, number]) => number > 0); + }, [filterType, filteredTransactions, categories]); return ( - - - History - {/* Pass the calculated total to BudgetChart */} - - - - - Spending Trend - - {/* */} - + + + + + Spending + - - Time Range: - setSelectedTimeRange(itemValue)} - style={styles.timeRangePicker} - > - - - - - - - - - - {/* Sorting Controls */} - - setShowSortOptions(!showSortOptions)} - style={styles.button} - > - Sort - - - - {/* Filter Controls */} - - - Filter + + + + + Top Categories + + + - - - - {/* Filter Selection UI */} - {showFilterOptions && ( - - Filter by: - - - setFilterType("month")} - > - Month - - - setFilterType("category")} - > - Category - - - - {filterType === "month" && ( - setSelectedMonth(itemValue)} - style={styles.filterPicker} - > - {getAvailableMonths().map((month) => ( - - ))} - - )} - - {filterType === "category" && ( - setSelectedCategory(itemValue)} - style={styles.filterPicker} - > - {categories.map((category) => ( - - ))} - - )} - - setShowFilterOptions(false)} - > - Apply Filter - - - {filterType !== "none" && ( - - Clear Filter - + + + {/* Filter Selection UI */} + {showSettings && ( + + Filter + + + setFilterType(newValue as FilterOption) + } + /> + + + {filterType === "Month" && ( + )} - - - )} - - {showSortOptions && ( - - Sort by: - - setSortBy(itemValue)} - style={styles.filterPicker} - > - - - - - - - setSortOrder((prev) => (prev === "asc" ? "desc" : "asc")) - } - style={styles.applyButton} - > - - {sortOrder === "asc" ? "Ascending 🔼" : "Descending 🔽"} - - - - )} - - {/* Display filter information */} - {filterType !== "none" && ( - - - Filtering by:{" "} - {filterType === "month" - ? `Month: ${formatMonth(selectedMonth)}` - : `Category: ${selectedCategory}`} - - - Total: ${totalAmount.toFixed(2)} - - - )} - - - - + + {filterType === "Category" && ( + + )} + + + Sort + + + + {sortIsAscending ? "Ascending" : "Descending"} + + + + + + setSortBy(newValue as SortOption) + } + /> + + Reset + + )} + + + {categoryTotals.slice(0, 2).map((category) => ( + + } + flexBasis={0} + /> + ))} + + + + + ); } - -const styles = StyleSheet.create({ - scrollContent: { - flexGrow: 1, - }, - homeContainer: { - flex: 1, - minHeight: "100%", - backgroundColor: "#00629B", - alignItems: "center", - paddingVertical: 20, - paddingHorizontal: 20, - flexDirection: "column", - gap: 10, - }, - Title: { - fontWeight: "bold", - fontSize: 30, - width: "100%", - color: "#FFFFFF", - paddingVertical: 10, - textAlign: "center", - }, - graphContainer: { - backgroundColor: "#FFFFFF", - padding: 15, - borderRadius: 10, - width: "90%", - alignItems: "center", - marginVertical: 10, - }, - timeRangePickerContainer: { - flexDirection: "row", - alignItems: "center", - marginTop: 15, - width: "100%", - justifyContent: "center", - }, - timeRangeLabel: { - fontSize: 16, - fontWeight: "600", - marginRight: 10, - }, - timeRangePicker: { - height: 50, - width: 150, - backgroundColor: "#E6E6E6", - borderRadius: 5, - }, - filterSortContainer: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginVertical: 16, // optional - width: "50%", - }, - filterButtonsContainer: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginVertical: 16, // optional - }, - sortingSection: { - alignItems: "center", - }, - filterSection: { - alignItems: "center", - flexDirection: "row", - }, - picker: { - height: 50, - width: 150, - backgroundColor: "#E6E6E6", - borderRadius: 5, - marginBottom: 5, - }, - button: { - backgroundColor: "#4CAF50", - padding: 10, - borderRadius: 5, - marginHorizontal: 5, - }, - resetButton: { - backgroundColor: "#f44336", - }, - buttonText: { - color: "#E6E6E6", - fontWeight: "bold", - }, - filterOptions: { - backgroundColor: "#E6E6E6", - padding: 15, - borderRadius: 10, - width: "90%", - alignItems: "center", - marginVertical: 10, - }, - filterTitle: { - fontSize: 18, - fontWeight: "bold", - marginBottom: 10, - }, - filterTypeButtons: { - flexDirection: "row", - justifyContent: "space-around", - width: "80%", - marginBottom: 15, - }, - filterTypeButton: { - paddingVertical: 8, - paddingHorizontal: 20, - borderRadius: 20, - backgroundColor: "#e0e0e0", - }, - selectedFilterType: { - backgroundColor: "#4CAF50", - }, - filterTypeText: { - fontWeight: "bold", - }, - filterPicker: { - height: 50, - width: "100%", - marginBottom: 10, - }, - applyButton: { - backgroundColor: "#4CAF50", - paddingVertical: 10, - paddingHorizontal: 20, - borderRadius: 5, - marginTop: 5, - }, - activeFilterContainer: { - backgroundColor: "rgba(255,255,255,0.7)", - padding: 8, - borderRadius: 5, - marginBottom: 5, - }, - activeFilterText: { - fontWeight: "bold", - }, -}); diff --git a/frontend/components/NewTransaction/NewTransactionButton.tsx b/frontend/components/NewTransaction/NewTransactionButton.tsx index 7989bd1..f85a81f 100644 --- a/frontend/components/NewTransaction/NewTransactionButton.tsx +++ b/frontend/components/NewTransaction/NewTransactionButton.tsx @@ -126,19 +126,11 @@ export default function NewTransactionButton(props: any) { style={styles.picker} > - {/* <<<<<<< HEAD - - - - - -======= */} - {/* >>>>>>> main */} { - const dateKey = transaction.date.slice(0, 10); - if (!acc[dateKey]) acc[dateKey] = []; - acc[dateKey].push(transaction); - return acc; - }, {}); - return ( - - Transactions - - - {Object.entries(groupedByDate).map(([date, trans]: any) => ( - - {date} - {trans.map((transaction: any, index: number) => ( - - - - - - handleDelete(transaction.id)} - > - ✕ - - - {index < trans.length - 1 && ( - - )} - - ))} - - ))} - - - + + {transactions.map((transaction: any) => ( + handleDelete(transaction.id)} + key={transaction.id} + /> + ))} + ); } - -const styles = StyleSheet.create({ - HistoryContainer: { - backgroundColor: "#E6E6E6", - width: "100%", - flex: 1, - borderRadius: 15, - padding: 15, - gap: 5, - shadowRadius: 12, - shadowOpacity: 0.4, - }, - title: { - fontSize: 20, - fontWeight: "500", - }, - recentTranactions: { - flexDirection: "column", - gap: 20, - }, - row: { - flexDirection: "column", - }, - dates: { - fontSize: 18, - fontWeight: "500", - opacity: 0.4, - }, - transactionContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - width: "100%", - }, - deleteButton: { - fontSize: 18, - color: "red", - paddingHorizontal: 10, - fontWeight: "bold", - }, - separator: { - width: "100%", - height: 2, - backgroundColor: "black", - opacity: 0.2, - borderRadius: 2, - }, -}); diff --git a/frontend/components/TransactionHistory/NewTransactionRow.tsx b/frontend/components/TransactionHistory/NewTransactionRow.tsx new file mode 100644 index 0000000..e1eff61 --- /dev/null +++ b/frontend/components/TransactionHistory/NewTransactionRow.tsx @@ -0,0 +1,116 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useEffect, useState } from "react"; +import { XStack, YStack } from "tamagui"; +import { AppText } from "../primitives/AppText"; +import { TouchableOpacity } from "react-native-gesture-handler"; + +interface Props { + name: string; + category: string; + date: string; + amount: string; + deleteCallback: () => void; +} + +/* + this is the container for every row in the transaction history, which includes the icon for the transaction, + name of transaction, date of transaction, and the amount. + + props - this component takes props for the name, date, amount of the transactions + */ +export default function NewTransactionRow(props: Props) { + const [icon, setIcon] = useState(""); + const categoryIconMapping: { [key: string]: string } = { + Food: "fast-food-outline", + Shopping: "pricetag-outline", + Transportation: "bus-outline", + Subscriptions: "calendar-outline", + Other: "planet-outline", + }; + + useEffect(() => { + const iconName: string = + categoryIconMapping[props.category] || "card-outline"; + setIcon(iconName); + }, []); + + const year = parseInt(props.date.substring(0, 4)); + const month = parseInt(props.date.substring(5, 7)); + const day = parseInt(props.date.substring(8, 10)); + const transactionDate = Date.UTC(year, month - 1, day); + const todayDate = Date.now(); + const deltaDay = Math.floor( + (todayDate - transactionDate) / (24 * 60 * 60 * 1000), + ); + + let relativeDate = ""; + if (deltaDay == 0) { + relativeDate = "Today"; + } else if (deltaDay == 1) { + relativeDate = "Yesterday"; + } else if (deltaDay < 7) { + relativeDate = `${deltaDay} day${deltaDay == 1 ? "" : "s"} ago`; + } else if (deltaDay < 28) { + const deltaWeek = Math.round(deltaDay / 7); + relativeDate = `${deltaWeek} week${deltaWeek == 1 ? "" : "s"} ago`; + } else if (deltaDay < 365) { + const deltaMonth = Math.round(deltaDay / 30); + relativeDate = `${deltaMonth} month${deltaMonth == 1 ? "" : "s"} ago`; + } else { + const deltaYear = Math.round(deltaDay / 365); + relativeDate = `${deltaYear} year${deltaYear == 1 ? "" : "s"} ago`; + } + + return ( + + + + + + + + {props.name} + + {`${props.category} • ${relativeDate}`} + + + + {`-$${props.amount}`} + + ✕ + + + + ); +} diff --git a/frontend/components/TransactionHistory/TransactionRow.tsx b/frontend/components/TransactionHistory/TransactionRow.tsx index 17bb382..ccd26f0 100644 --- a/frontend/components/TransactionHistory/TransactionRow.tsx +++ b/frontend/components/TransactionHistory/TransactionRow.tsx @@ -11,13 +11,6 @@ import { useEffect, useState } from "react"; */ export default function TransactionRow(props: any) { const [icon, setIcon] = useState(""); - // <<<<<<< HEAD - // const categoryIconMapping: { [key: number]: string } = { - // 6: "fast-food-outline", - // 7: "pricetag-outline", - // 8: "bus-outline", - // 9: "calendar-outline", - // ======= const categoryIconMapping: { [key: string]: string } = { Food: "fast-food-outline", Shopping: "pricetag-outline", diff --git a/frontend/components/primitives/AppSelect.tsx b/frontend/components/primitives/AppSelect.tsx new file mode 100644 index 0000000..9d04e1a --- /dev/null +++ b/frontend/components/primitives/AppSelect.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Select, SelectProps } from "tamagui"; +import { Ionicons } from "@expo/vector-icons"; + +interface AppSelectProps extends SelectProps { + options: string[]; + value?: string; + defaultValue?: string; + onValueChange?: (newValue: string) => void; +} + +export const AppSelect: React.FC = ({ + options, + value, + defaultValue, + onValueChange, + ...props +}) => { + return ( + + ); +}; diff --git a/frontend/components/primitives/AppSwitch.tsx b/frontend/components/primitives/AppSwitch.tsx new file mode 100644 index 0000000..310f2ea --- /dev/null +++ b/frontend/components/primitives/AppSwitch.tsx @@ -0,0 +1,23 @@ +import { Switch, SwitchProps, styled } from "tamagui"; + +interface AppSwitchProps extends SwitchProps { + checked: boolean; +} + +export const AppSwitch: React.FC = ({ checked, ...props }) => { + return ( + + + + ); +}; diff --git a/frontend/components/primitives/SegmentedControl.tsx b/frontend/components/primitives/SegmentedControl.tsx index b3f2a0a..7095891 100644 --- a/frontend/components/primitives/SegmentedControl.tsx +++ b/frontend/components/primitives/SegmentedControl.tsx @@ -2,21 +2,25 @@ import React, { useState } from "react"; import { XStack, YStack } from "tamagui"; import { AppText } from "./AppText"; -const PERIODS = ["1D", "1W", "1M", "1Y"] as const; -type Period = (typeof PERIODS)[number]; - interface SegmentedControlProps { - onValueChange?: (value: Period) => void; - defaultValue?: Period; + value?: string; + onValueChange?: (value: string) => void; + periods?: string[]; + defaultValue?: string; } export const SegmentedControl: React.FC = ({ + value, onValueChange, + periods = ["1D", "1W", "1M", "1Y"], defaultValue = "1M", }) => { - const [active, setActive] = useState(defaultValue); + const [isActive, setActive] = useState(defaultValue); + if (!value) { + value = isActive; + } - const handlePress = (period: Period) => { + const handlePress = (period: string) => { setActive(period); onValueChange?.(period); }; @@ -27,9 +31,10 @@ export const SegmentedControl: React.FC = ({ borderRadius="$7" // Pill shape container padding="$1" width="100%" + flex={1} > - {PERIODS.map((period) => { - const isActive = active === period; + {periods.map((period) => { + const isActive = value === period; return (