Skip to content

Commit

Permalink
feat(mobile): add update category
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Jul 8, 2024
1 parent 4f2a0d0 commit f84055d
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 36 deletions.
2 changes: 1 addition & 1 deletion apps/mobile/app/(app)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default function SettingsScreen() {
}
/>
</Link>
<Link href="/categories" asChild>
<Link href="/category" asChild>
<MenuItem
label={t(i18n)`Categories`}
icon={ShapesIcon}
Expand Down
13 changes: 10 additions & 3 deletions apps/mobile/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ export default function AuthenticatedLayout() {
}}
/>
<Stack.Screen
name="categories/index"
name="category/index"
options={{
headerTitle: t(i18n)`Categories`,
headerRight: () => (
<Link href="/categories/new-category" asChild>
<Link href="/category/new-category" asChild>
<Button size="icon" variant="ghost">
<PlusIcon className="size-6 text-primary" />
</Button>
Expand All @@ -107,12 +107,19 @@ export default function AuthenticatedLayout() {
}}
/>
<Stack.Screen
name="categories/new-category"
name="category/new-category"
options={{
presentation: 'modal',
headerTitle: t(i18n)`New category`,
}}
/>
<Stack.Screen
name="category/[categoryId]"
options={{
// presentation: 'modal',
headerTitle: t(i18n)`Edit category`,
}}
/>
</Stack>
)
}
54 changes: 54 additions & 0 deletions apps/mobile/app/(app)/category/[categoryId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { CategoryForm } from '@/components/category/category-form'
import { Text } from '@/components/ui/text'
import { updateCategory } from '@/mutations/category'
import { categoryQueries, useCategories } from '@/queries/category'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { Alert, ScrollView, View } from 'react-native'

export default function EditCategoryScreen() {
const { categoryId } = useLocalSearchParams<{ categoryId: string }>()
const { data: categories = [] } = useCategories()
const queryClient = useQueryClient()
const router = useRouter()

const { mutateAsync: mutateUpdate } = useMutation({
mutationFn: updateCategory,
onError(error) {
Alert.alert(error.message)
},
onSuccess() {
router.back()
},
async onSettled() {
await queryClient.invalidateQueries({
queryKey: categoryQueries.list._def,
})
},
throwOnError: true,
})

const category = categories.find((category) => category.id === categoryId)

if (!category) {
return (
<View className="flex-1 bg-card items-center justify-center">
<Text className="text-muted-foreground">Category not found</Text>
</View>
)
}

return (
<ScrollView className="bg-card px-6 py-3">
<CategoryForm
onSubmit={(values) => mutateUpdate({ id: category.id, data: values })}
hiddenFields={['type']}
defaultValues={{
name: category?.name,
icon: category?.icon!,
type: category?.type,
}}
/>
</ScrollView>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function CategoriesScreen() {
label={t(i18n)`New ${section.key.toLowerCase()}`}
onPress={() =>
router.push({
pathname: '/categories/new-category',
pathname: '/category/new-category',
params: { type: section.key },
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { Alert, View } from 'react-native'

export default function CreateCategoryScreen() {
const router = useRouter()
const { type } = useLocalSearchParams<{ type?: CategoryTypeType }>()
const { type = 'EXPENSE' } = useLocalSearchParams<{
type?: CategoryTypeType
}>()
const queryClient = useQueryClient()
const { mutateAsync } = useMutation({
mutationFn: createCategory,
Expand Down
62 changes: 40 additions & 22 deletions apps/mobile/components/category/category-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { SelectCategoryIconField } from './select-category-icon-field'
type CategoryFormProps = {
onSubmit: (data: CategoryFormValues) => void
defaultValues?: Partial<CategoryFormValues>
hiddenFields?: Array<'type'>
}

export const CategoryForm = ({
onSubmit,
defaultValues,
hiddenFields = [],
}: CategoryFormProps) => {
const { i18n } = useLingui()
const nameInputRef = useRef<TextInput>(null)
Expand All @@ -28,11 +30,14 @@ export const CategoryForm = ({
resolver: zodResolver(zCategoryFormValues),
defaultValues: {
name: '',
type: 'EXPENSE',
icon: 'CreditCard',
...defaultValues,
type: defaultValues?.type || 'EXPENSE',
},
})
const type = categoryForm.watch('type')

const isTypeHidden = hiddenFields.includes('type')

return (
<FormProvider {...categoryForm}>
Expand All @@ -45,34 +50,47 @@ export const CategoryForm = ({
autoCapitalize="none"
autoFocus={!defaultValues}
className="!pl-[62px]"
disabled={categoryForm.formState.isLoading}
leftSection={
<SelectCategoryIconField
type={type}
disabled={categoryForm.formState.isLoading}
onSelect={() => nameInputRef.current?.focus()}
/>
}
/>

<Text className="font-medium">{t(i18n)`Type`}</Text>
<Controller
control={categoryForm.control}
name="type"
render={({ field }) => (
<Tabs
value={field.value}
className="-mt-3"
onValueChange={field.onChange}
>
<TabsList>
<TabsTrigger value="EXPENSE">
<Text>{t(i18n)`Expense`}</Text>
</TabsTrigger>
<TabsTrigger value="INCOME">
<Text>{t(i18n)`Income`}</Text>
</TabsTrigger>
</TabsList>
</Tabs>
)}
/>
{!isTypeHidden && (
<>
<Text className="font-medium">{t(i18n)`Type`}</Text>
<Controller
control={categoryForm.control}
name="type"
render={({ field }) => (
<Tabs
value={field.value}
className="-mt-3"
onValueChange={field.onChange}
>
<TabsList>
<TabsTrigger
disabled={categoryForm.formState.isLoading}
value="EXPENSE"
>
<Text>{t(i18n)`Expense`}</Text>
</TabsTrigger>
<TabsTrigger
disabled={categoryForm.formState.isLoading}
value="INCOME"
>
<Text>{t(i18n)`Income`}</Text>
</TabsTrigger>
</TabsList>
</Tabs>
)}
/>
</>
)}

<SubmitButton
onPress={categoryForm.handleSubmit(onSubmit)}
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/components/category/category-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const CategoryItem: FC<CategoryItemProps> = ({ category }) => {
asChild
push
href={{
pathname: '/categories/[categoryId]',
pathname: '/category/[categoryId]',
params: { categoryId: category.id },
}}
>
Expand Down
31 changes: 26 additions & 5 deletions apps/mobile/components/category/select-category-icon-field.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'

import { WALLET_ICONS } from '@/lib/icons/wallet-icons'
import {
CATEGORY_EXPENSE_ICONS,
CATEGORY_INCOME_ICONS,
} from '@/lib/icons/category-icons'
import { sleep } from '@/lib/utils'
import type { CategoryTypeType } from '@6pm/validation'
import { useController } from 'react-hook-form'
import { Keyboard } from 'react-native'
import { FullWindowOverlay } from 'react-native-screens'
import GenericIcon from '../common/generic-icon'
import { IconGridSheet } from '../common/icon-grid-sheet'
import { Button } from '../ui/button'

export function SelectCategoryIconField({
onSelect,
disabled,
type,
}: {
onSelect?: (currency: string) => void
disabled?: boolean
type: CategoryTypeType
}) {
const sheetRef = useRef<BottomSheetModal>(null)
const {
field: { onChange, onBlur, value },
// fieldState,
} = useController({ name: 'icon' })

const icons = useMemo(
() => (type === 'EXPENSE' ? CATEGORY_EXPENSE_ICONS : CATEGORY_INCOME_ICONS),
[type],
)

return (
<>
<Button
variant="ghost"
disabled={disabled}
onPress={() => {
Keyboard.dismiss()
sheetRef.current?.present()
Expand All @@ -36,6 +52,7 @@ export function SelectCategoryIconField({
index={0}
enableDynamicSizing
enablePanDownToClose
enableDismissOnClose
keyboardBehavior="extend"
backdropComponent={(props) => (
<BottomSheetBackdrop
Expand All @@ -45,13 +62,17 @@ export function SelectCategoryIconField({
enableTouchThrough
/>
)}
containerComponent={(props) => (
<FullWindowOverlay>{props.children}</FullWindowOverlay>
)}
>
<IconGridSheet
icons={WALLET_ICONS}
icons={icons}
value={value}
onSelect={(icon) => {
onChange(icon)
onSelect={async (icon) => {
sheetRef.current?.close()
await sleep(500)
onChange(icon)
onBlur()
onSelect?.(icon)
}}
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/components/form-fields/input-field.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { cn } from '@/lib/utils'
import { forwardRef } from 'react'
import { useController } from 'react-hook-form'
import { Text, type TextInputProps, View, type TextInput } from 'react-native'
import { Text, type TextInput, type TextInputProps, View } from 'react-native'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { forwardRef } from 'react'

type InputFieldProps = TextInputProps & {
name: string
label?: string
leftSection?: React.ReactNode
rightSection?: React.ReactNode
disabled?: boolean
}

export const InputField = forwardRef(
Expand All @@ -20,6 +21,7 @@ export const InputField = forwardRef(
leftSection,
rightSection,
className,
disabled,
...props
}: InputFieldProps,
ref: React.Ref<TextInput>,
Expand Down Expand Up @@ -47,6 +49,7 @@ export const InputField = forwardRef(
leftSection && 'pl-10',
rightSection && 'pr-10',
)}
editable={!disabled}
{...props}
/>
{rightSection && (
Expand Down
49 changes: 49 additions & 0 deletions apps/mobile/lib/icons/category-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { icons } from 'lucide-react-native'

export const CATEGORY_EXPENSE_ICONS: Array<keyof typeof icons> = [
'WalletMinimal',
'Coins',
'Banknote',
'Bitcoin',
'CreditCard',
'Gem',
'HandCoins',
'Handshake',
'PiggyBank',
'SmartphoneNfc',
'BadgeCent',
'BadgeDollarSign',
'BadgeEuro',
'BadgeIndianRupee',
'BadgeJapaneseYen',
'BadgePoundSterling',
'BadgeRussianRuble',
'BadgeSwissFranc',
// 'Paintbrush',
// 'BrickWall',
// 'CookingPot',
]

export const CATEGORY_INCOME_ICONS: Array<keyof typeof icons> = [
'WalletMinimal',
'Coins',
'Banknote',
'Bitcoin',
'CreditCard',
'Gem',
'HandCoins',
'Handshake',
'PiggyBank',
'SmartphoneNfc',
'BadgeCent',
'BadgeDollarSign',
'BadgeEuro',
'BadgeIndianRupee',
'BadgeJapaneseYen',
'BadgePoundSterling',
'BadgeRussianRuble',
'BadgeSwissFranc',
// 'Paintbrush',
// 'BrickWall',
// 'CookingPot',
].reverse() as Array<keyof typeof icons>
4 changes: 4 additions & 0 deletions apps/mobile/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

export async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
Loading

0 comments on commit f84055d

Please sign in to comment.