Skip to content

Commit

Permalink
feat(mobile): add category list
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Jun 27, 2024
1 parent 89caa12 commit a981c39
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 31 deletions.
76 changes: 60 additions & 16 deletions apps/mobile/app/(app)/categories/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,67 @@
import { useState } from 'react'
import { RefreshControl } from 'react-native'
import { ScrollView, Text } from 'react-native'
import { CategoryItem } from '@/components/category/category-item'
import { AddNewButton } from '@/components/common/add-new-button'
import { Skeleton } from '@/components/ui/skeleton'
import { Text } from '@/components/ui/text'
import { useCategories } from '@/queries/category'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useRouter } from 'expo-router'
import { SectionList } from 'react-native'

export default function CategoriesScreen() {
const [isLoading, setIsLoading] = useState(false)
const { i18n } = useLingui()
const router = useRouter()
const { data: categories = [], isLoading, refetch } = useCategories()

const refetch = () => {
setIsLoading(true)
setTimeout(() => setIsLoading(false), 2000)
}
const incomeCategories = categories.filter(
(category) => category.type === 'INCOME',
)
const expenseCategories = categories.filter(
(category) => category.type === 'EXPENSE',
)

const sections = [
{ key: 'INCOME', title: 'Incomes', data: incomeCategories },
{ key: 'EXPENSE', title: 'Expenses', data: expenseCategories },
]

return (
<ScrollView
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refetch} />
}
className="py-3 px-6 bg-card flex-1"
>
<Text className="text-muted-foreground">Expenses</Text>
</ScrollView>
<SectionList
className="py-3 bg-card flex-1"
refreshing={isLoading}
onRefresh={refetch}
sections={sections}
keyExtractor={(item) => item.id}
renderItem={({ item: category }) => <CategoryItem category={category} />}
renderSectionHeader={({ section: { title } }) => (
<Text className="text-muted-foreground mx-6">{title}</Text>
)}
renderSectionFooter={({ section }) => (
<>
{!section.data.length &&
(isLoading ? (
<>
<Skeleton className="mx-6 mb-5 mt-3 h-4 rounded-full" />
<Skeleton className="mx-6 mb-5 mt-3 h-4 rounded-full" />
<Skeleton className="mx-6 mb-5 mt-3 h-4 rounded-full" />
</>
) : (
<Text className="font-sans text-muted-foreground text-center mt-6 mb-9">
{t(i18n)`empty`}
</Text>
))}
<AddNewButton
label={t(i18n)`New ${section.key.toLowerCase()}`}
onPress={() =>
router.push({
pathname: '/categories/new-category',
params: { type: section.key },
})
}
className="mb-6"
/>
</>
)}
/>
)
}
15 changes: 12 additions & 3 deletions apps/mobile/app/(app)/categories/new-category.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CategoryForm } from '@/components/category/category-form'
import { createCategory } from '@/mutations/category'
import { useMutation } from '@tanstack/react-query'
import { useRouter } from 'expo-router'
import { categoryQueries } from '@/queries/category'
import type { CategoryTypeType } from '@6pm/validation'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { Alert, View } from 'react-native'

export default function CreateCategoryScreen() {
const router = useRouter()
const { type } = useLocalSearchParams<{ type?: CategoryTypeType }>()
const queryClient = useQueryClient()
const { mutateAsync } = useMutation({
mutationFn: createCategory,
onError(error) {
Expand All @@ -14,11 +18,16 @@ export default function CreateCategoryScreen() {
onSuccess() {
router.back()
},
async onSettled() {
await queryClient.invalidateQueries({
queryKey: categoryQueries.list._def,
})
},
})

return (
<View className="py-3 px-6 bg-card h-screen">
<CategoryForm onSubmit={mutateAsync} />
<CategoryForm onSubmit={mutateAsync} defaultValues={{ type }} />
</View>
)
}
20 changes: 14 additions & 6 deletions apps/mobile/app/+not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { Link, Stack } from 'expo-router'
import { Link, Stack, useRouter } from 'expo-router'
import { View } from 'react-native'

import { Button } from '@/components/ui/button'
import { Text } from '@/components/ui/text'

export default function NotFoundScreen() {
const router = useRouter()
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View className="flex-1 items-center justify-center p-4 gap-4">
<Text className='font-sans text-primary font-medium'>This screen doesn't exist.</Text>
<Link href="/" asChild={true}>
<Button>
<Text>Go to home screen!</Text>
<Text className="font-sans text-primary font-medium">
This screen doesn't exist.
</Text>
<View className="flex-row gap-4">
<Button variant="outline" onPress={() => router.back()}>
<Text>Go back</Text>
</Button>
</Link>
<Link href="/" asChild={true}>
<Button>
<Text>Go to home screen!</Text>
</Button>
</Link>
</View>
</View>
</>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/components/category/category-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SelectCategoryIconField } from './select-category-icon-field'

type CategoryFormProps = {
onSubmit: (data: CategoryFormValues) => void
defaultValues?: CategoryFormValues
defaultValues?: Partial<CategoryFormValues>
}

export const CategoryForm = ({
Expand Down
33 changes: 33 additions & 0 deletions apps/mobile/components/category/category-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Category } from '@6pm/validation'
import { Link } from 'expo-router'
import type { FC } from 'react'
import GenericIcon from '../common/generic-icon'
import { MenuItem } from '../common/menu-item'

type CategoryItemProps = {
category: Category
}

export const CategoryItem: FC<CategoryItemProps> = ({ category }) => {
return (
<Link
asChild
push
href={{
pathname: '/categories/[categoryId]',
params: { categoryId: category.id },
}}
>
<MenuItem
label={category.name}
icon={() => (
<GenericIcon
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
name={category.icon as any}
className="size-6 text-foreground"
/>
)}
/>
</Link>
)
}
14 changes: 9 additions & 5 deletions apps/mobile/components/wallet/wallet-account-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UserWalletAccount } from '@6pm/api'
import type { UserWalletAccount } from '@6pm/validation'
import { Link } from 'expo-router'
import { ChevronRightIcon } from 'lucide-react-native'
import type { FC } from 'react'
Expand All @@ -13,10 +13,14 @@ type WalletAccountItemProps = {

export const WalletAccountItem: FC<WalletAccountItemProps> = ({ data }) => {
return (
<Link asChild push href={{
pathname: "/wallet/[walletId]",
params: { walletId: data.id }
}}>
<Link
asChild
push
href={{
pathname: '/wallet/[walletId]',
params: { walletId: data.id },
}}
>
<MenuItem
label={data.name}
icon={() => (
Expand Down
24 changes: 24 additions & 0 deletions apps/mobile/queries/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getHonoClient } from '@/lib/client'
import { CategorySchema } from '@6pm/validation'
import { createQueryKeys } from '@lukemorales/query-key-factory'
import { useQuery } from '@tanstack/react-query'

export const categoryQueries = createQueryKeys('category', {
list: () => ({
queryKey: [{}],
queryFn: async () => {
const hc = await getHonoClient()
const res = await hc.v1.categories.$get()
if (!res.ok) {
throw new Error(await res.text())
}

const items = await res.json()
return items.map((item) => CategorySchema.parse(item))
},
}),
})

export function useCategories() {
return useQuery(categoryQueries.list())
}

0 comments on commit a981c39

Please sign in to comment.