diff --git a/apps/mobile/app/(app)/(tabs)/budgets.tsx b/apps/mobile/app/(app)/(tabs)/budgets.tsx index 59daa868..3fb4e12a 100644 --- a/apps/mobile/app/(app)/(tabs)/budgets.tsx +++ b/apps/mobile/app/(app)/(tabs)/budgets.tsx @@ -3,19 +3,122 @@ import { BudgetStatistic } from '@/components/budget/budget-statistic' import { BurndownChart } from '@/components/budget/burndown-chart' import { Toolbar } from '@/components/common/toolbar' import { Skeleton } from '@/components/ui/skeleton' +import { Text } from '@/components/ui/text' import { useColorScheme } from '@/hooks/useColorScheme' import { theme } from '@/lib/theme' import { useBudgetList } from '@/stores/budget/hooks' +import type { BudgetPopulated } from '@6pm/validation' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { LinearGradient } from 'expo-linear-gradient' -import { SectionList, Text, View } from 'react-native' +import { Dimensions, SectionList, View } from 'react-native' +import Animated, { + Extrapolation, + interpolate, + useAnimatedScrollHandler, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' +const { height } = Dimensions.get('screen') + +const chartHeight = 187 +const spacing = 16 + +const AnimatedSectionList = Animated.createAnimatedComponent( + SectionList, +) + export default function BudgetsScreen() { const { i18n } = useLingui() const { bottom } = useSafeAreaInsets() const { colorScheme } = useColorScheme() + const headerAnimation = useSharedValue(0) + const scrollY = useSharedValue(0) + const headerHeight = useSharedValue(height) + + const dummyHeaderStyle = useAnimatedStyle(() => { + return { + height: headerHeight.value, + } + }) + + const summaryStyle = useAnimatedStyle(() => { + const extraSectionHeaderSpacing = spacing * 2 + return { + opacity: interpolate( + headerAnimation.value, + [ + 0, + chartHeight, + chartHeight + extraSectionHeaderSpacing, + headerHeight.value, + ], + [1, 1, 1, 0], + ), + transform: [ + { + translateY: interpolate( + headerAnimation.value, + [ + 0, + chartHeight, + chartHeight + extraSectionHeaderSpacing, + chartHeight + extraSectionHeaderSpacing + 1, + ], + [0, 0, 0, -1], + ), + }, + ], + } + }) + + const chartStyle = useAnimatedStyle(() => ({ + transform: [ + { + perspective: chartHeight * 5, + }, + { + translateY: interpolate( + headerAnimation.value, + [0, chartHeight], + [0, -chartHeight / 2 - spacing], + Extrapolation.EXTEND, + ), + }, + { + rotateX: `${interpolate( + headerAnimation.value, + [0, chartHeight], + [0, 90], + Extrapolation.CLAMP, + )}deg`, + }, + { + scaleY: interpolate( + headerAnimation.value, + [-chartHeight, 0], + [1.4, 1], + Extrapolation.CLAMP, + ), + }, + ], + opacity: interpolate( + headerAnimation.value, + [0, chartHeight / 2, chartHeight], + [1, 0.6, 0], + Extrapolation.CLAMP, + ), + })) + + const onScroll = useAnimatedScrollHandler((event) => { + const { y } = event.contentOffset + scrollY.value = y + headerAnimation.value = y + }) + const { spendingBudgets, savingBudgets, @@ -35,15 +138,31 @@ export default function BudgetsScreen() { return ( - - - - - } - className="bg-card flex-1" - contentContainerClassName="px-6" + { + if (headerHeight.value === ev.nativeEvent.layout.height) { + return + } + headerHeight.value = withTiming(ev.nativeEvent.layout.height, { + duration: 0, + }) + }} + > + + + + + + + + } contentContainerStyle={{ paddingBottom: bottom + 32 }} refreshing={isRefetching} onRefresh={refetch} @@ -51,7 +170,9 @@ export default function BudgetsScreen() { keyExtractor={(item) => item.id} renderItem={({ item: budget }) => } renderSectionHeader={({ section: { title } }) => ( - {title} + + {title} + )} ListFooterComponent={ (isLoading || isRefetching) && !sections.length ? ( diff --git a/apps/mobile/components/budget/budget-item.tsx b/apps/mobile/components/budget/budget-item.tsx index f9604839..3fa8d7a8 100644 --- a/apps/mobile/components/budget/budget-item.tsx +++ b/apps/mobile/components/budget/budget-item.tsx @@ -59,7 +59,7 @@ export const BudgetItem: FC = ({ budget }) => { params: { budgetId: budget.id }, }} > - + {budget.name} diff --git a/apps/mobile/components/budget/budget-statistic.tsx b/apps/mobile/components/budget/budget-statistic.tsx index 55b1bb21..1ba5fcf5 100644 --- a/apps/mobile/components/budget/budget-statistic.tsx +++ b/apps/mobile/components/budget/budget-statistic.tsx @@ -21,7 +21,7 @@ export function BudgetStatistic({ {totalRemaining?.toLocaleString() || '0.00'}{' '} VND - + {t(i18n)`Left this month`} @@ -30,7 +30,7 @@ export function BudgetStatistic({ {remainingPerDay?.toLocaleString() || '0.00'}{' '} VND - + {t(i18n)`Left per day`}