Skip to content

Commit

Permalink
feat(mobile): [Budget] add budget list
Browse files Browse the repository at this point in the history
  • Loading branch information
Quốc Khánh authored and bkdev98 committed Jul 20, 2024
1 parent 7b4770c commit 5a2c1db
Show file tree
Hide file tree
Showing 19 changed files with 899 additions and 92 deletions.
6 changes: 6 additions & 0 deletions apps/mobile/app/(app)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export default function TabLayout() {
name="budgets"
options={{
headerTitle: t(i18n)`Budgets`,
headerTitleStyle: {
fontFamily: 'Be Vietnam Pro Medium',
fontSize: 16,
color: theme[colorScheme ?? 'light'].primary,
marginLeft: 5,
},
tabBarShowLabel: false,
tabBarIcon: ({ color }) => <LandPlotIcon color={color} />,
headerRight: () => (
Expand Down
70 changes: 68 additions & 2 deletions apps/mobile/app/(app)/(tabs)/budgets.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,76 @@
import { BudgetItem } from '@/components/budget/budget-item'
import { BudgetStatistic } from '@/components/budget/budget-statistic'
import { BurndownChart } from '@/components/budget/burndown-chart'
import { Toolbar } from '@/components/common/toolbar'
import { Text, View } from 'react-native'
import { Skeleton } from '@/components/ui/skeleton'
import { useColorScheme } from '@/hooks/useColorScheme'
import { theme } from '@/lib/theme'
import { useBudgetList } from '@/stores/budget/hooks'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { LinearGradient } from 'expo-linear-gradient'
import { SectionList, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

export default function BudgetsScreen() {
const { i18n } = useLingui()
const { bottom } = useSafeAreaInsets()
const { colorScheme } = useColorScheme()
const {
spendingBudgets,
savingBudgets,
investingBudgets,
debtBudgets,
isRefetching,
isLoading,
refetch,
} = useBudgetList()

const sections = [
{ key: 'SPENDING', title: t(i18n)`Spending`, data: spendingBudgets },
{ key: 'SAVING', title: t(i18n)`Saving`, data: savingBudgets },
{ key: 'INVESTING', title: t(i18n)`Investing`, data: investingBudgets },
{ key: 'DEBT', title: t(i18n)`Debt`, data: debtBudgets },
].filter(({ data }) => data.length)

return (
<View className="flex-1 bg-card">
<Text className="font-sans">Budgets Screen</Text>
<SectionList
ListHeaderComponent={
<View className="gap-6 pb-4 pt-8">
<BudgetStatistic totalRemaining={1000} remainingPerDay={100} />
<BurndownChart />
</View>
}
className="bg-card flex-1"
contentContainerClassName="px-6"
contentContainerStyle={{ paddingBottom: bottom + 32 }}
refreshing={isRefetching}
onRefresh={refetch}
sections={sections}
keyExtractor={(item) => item.id}
renderItem={({ item: budget }) => <BudgetItem budget={budget} />}
renderSectionHeader={({ section: { title } }) => (
<Text className="text-muted-foreground bg-card py-2">{title}</Text>
)}
ListFooterComponent={
(isLoading || isRefetching) && !sections.length ? (
<View className="gap-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</View>
) : null
}
/>
<LinearGradient
colors={[
colorScheme === 'dark' ? 'transparent' : '#ffffff00',
theme[colorScheme ?? 'light'].background,
]}
className="absolute bottom-0 left-0 right-0 h-36"
pointerEvents="none"
/>
<Toolbar />
</View>
)
Expand Down
11 changes: 2 additions & 9 deletions apps/mobile/app/(app)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Application from 'expo-application'

import { Logo } from '@/components/common/logo'
import { MenuItem } from '@/components/common/menu-item'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { UserAvatar } from '@/components/common/user-avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
Expand Down Expand Up @@ -44,14 +44,7 @@ export default function SettingsScreen() {
<View className="flex flex-1" />
<View className="flex flex-row px-4 py-3 items-center gap-2 justify-between">
<View className="flex flex-row items-center gap-3">
<Avatar alt="avatar" className="w-12 h-12">
<AvatarImage
source={{
uri: user?.imageUrl,
}}
/>
<AvatarFallback>QK</AvatarFallback>
</Avatar>
<UserAvatar user={user!} fallbackClassName="bg-card" />
<View>
<Badge variant="outline" className="self-start rounded-md mb-1">
<Text className="text-xs font-medium">Free</Text>
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/app/(app)/category/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default function CategoriesScreen() {
const { bottom } = useSafeAreaInsets()

const sections = [
{ key: 'INCOME', title: 'Incomes', data: incomeCategories },
{ key: 'EXPENSE', title: 'Expenses', data: expenseCategories },
{ key: 'INCOME', title: t(i18n)`Incomes`, data: incomeCategories },
{ key: 'EXPENSE', title: t(i18n)`Expenses`, data: expenseCategories },
]

return (
Expand Down
131 changes: 131 additions & 0 deletions apps/mobile/components/budget/budget-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { formatDuration, intervalToDuration } from 'date-fns'
import { Link } from 'expo-router'
import { type FC, useMemo } from 'react'
import { Pressable, View } from 'react-native'

import type { BudgetItem as BudgetItemData } from '@/stores/budget/store'
import { calculateBudgetPeriodStartEndDates } from '@6pm/utilities'
import { useUser } from '@clerk/clerk-expo'
import { ChevronRightIcon } from 'lucide-react-native'
import { AmountFormat } from '../common/amount-format'
import { CircularProgress } from '../common/circular-progress'
import { UserAvatar } from '../common/user-avatar'
import { Badge } from '../ui/badge'
import { Separator } from '../ui/separator'
import { Text } from '../ui/text'

type BudgetItemProps = {
budget: BudgetItemData
}

export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
const { i18n } = useLingui()
const { user } = useUser()

const remainingBalance = 2500000

const amountPerDay = 312500

const usagePercentage = 80

const remainingDays = useMemo(() => {
let periodEndDate: Date | null
if (budget.periodConfigs?.[0]?.type === 'CUSTOM') {
periodEndDate = budget.periodConfigs?.[0]?.endDate
} else {
const { endDate } = calculateBudgetPeriodStartEndDates({
anchorDate: new Date(),
type: budget.periodConfigs?.[0]?.type,
})
periodEndDate = endDate
}

if (!periodEndDate) {
return t(i18n)`Unknown`
}

const duration = formatDuration(
intervalToDuration({
start: new Date(),
end: periodEndDate,
}),
{
format: ['days', 'hours'],
delimiter: ',',
},
)

return t(i18n)`${duration?.split(',')[0]} left`
}, [budget.periodConfigs, i18n])

return (
<Link
asChild
push
href={{
pathname: '/budget/[budgetId]',
params: { budgetId: budget.id },
}}
>
<Pressable className="gap-4 mb-3 mt-1 justify-between p-4 border border-border rounded-lg">
<View className="flex-row items-center gap-6 justify-between">
<View className="gap-2">
<Text
numberOfLines={1}
className="flex-1 line-clamp-1 font-semibold text-lg"
>
{budget.name}
</Text>
<View className="flex-row items-center gap-2">
<UserAvatar
user={user!}
className="w-7 h-7"
fallbackLabelClassName="text-[10px]"
/>
<Badge variant="outline" className="rounded-full">
<Text className="text-sm capitalize">
{budget.periodConfigs[0].type}
</Text>
</Badge>
</View>
</View>
<View className="gap-2 items-center flex-row">
<CircularProgress progress={usagePercentage} />
<ChevronRightIcon className="size-6 text-foreground" />
</View>
</View>
<Separator />
<View className="flex-row items-center gap-6 justify-between">
<View className="gap-1">
<AmountFormat
amount={remainingBalance}
displayNegativeSign
className="text-xl"
/>
<Text
numberOfLines={1}
className="flex-1 line-clamp-1 text-muted-foreground text-sm"
>
{remainingDays}
</Text>
</View>
<View className="gap-1 justify-end">
<AmountFormat
amount={amountPerDay}
displayNegativeSign
className="text-xl"
/>
<Text
numberOfLines={1}
className="flex-1 line-clamp-1 text-right text-muted-foreground text-sm"
>
{t(i18n)`per day`}
</Text>
</View>
</View>
</Pressable>
</Link>
)
}
39 changes: 39 additions & 0 deletions apps/mobile/components/budget/budget-statistic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { View } from 'react-native'
import { Text } from '../ui/text'

type BudgetStatisticProps = {
totalRemaining: number
remainingPerDay: number
}

export function BudgetStatistic({
totalRemaining,
remainingPerDay,
}: BudgetStatisticProps) {
const { i18n } = useLingui()

return (
<View className="flex-row items-center gap-6 justify-between">
<View className="gap-1">
<Text className="font-semibold text-2xl">
{totalRemaining?.toLocaleString() || '0.00'}{' '}
<Text className="text-muted-foreground font-normal text-sm">VND</Text>
</Text>
<Text className="text-muted-foreground text-sm">
{t(i18n)`Left this month`}
</Text>
</View>
<View className="gap-1">
<Text className="font-semibold text-2xl text-right">
{remainingPerDay?.toLocaleString() || '0.00'}{' '}
<Text className="text-muted-foreground font-normal text-sm">VND</Text>
</Text>
<Text className="text-muted-foreground text-sm text-right">
{t(i18n)`Left per day`}
</Text>
</View>
</View>
)
}
5 changes: 5 additions & 0 deletions apps/mobile/components/budget/burndown-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { View } from 'react-native'

export function BurndownChart() {
return <View className="bg-gray-100 rounded-lg h-[187px] w-full" />
}
Loading

0 comments on commit 5a2c1db

Please sign in to comment.