diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index 71aa9188..9915f4ad 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -1,4 +1,4 @@ -import * as Application from 'expo-application'; +import * as Application from 'expo-application' import { Logo } from '@/components/common/logo' import { MenuItem } from '@/components/common/menu-item' @@ -91,7 +91,7 @@ export default function SettingsScreen() { } /> - + } + rightSection={ + + } /> - {t(i18n)`ver.`}{Application.nativeApplicationVersion} + {t(i18n)`ver.`} + {Application.nativeApplicationVersion} diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index bb2bd026..d3ec47fc 100644 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -25,21 +25,23 @@ export default function AuthenticatedLayout() { } return ( - , - }}> + , + }} + > ( - - ) + ), }} /> + ( + + + + ), + }} + /> + diff --git a/apps/mobile/app/(app)/categories/index.tsx b/apps/mobile/app/(app)/categories/index.tsx new file mode 100644 index 00000000..5e9bb60a --- /dev/null +++ b/apps/mobile/app/(app)/categories/index.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react' +import { RefreshControl } from 'react-native' +import { ScrollView, Text } from 'react-native' + +export default function CategoriesScreen() { + const [isLoading, setIsLoading] = useState(false) + + const refetch = () => { + setIsLoading(true) + setTimeout(() => setIsLoading(false), 2000) + } + + return ( + + } + className="py-3 px-6 bg-card flex-1" + > + Expenses + + ) +} diff --git a/apps/mobile/app/(app)/categories/new-category.tsx b/apps/mobile/app/(app)/categories/new-category.tsx new file mode 100644 index 00000000..738aa9ad --- /dev/null +++ b/apps/mobile/app/(app)/categories/new-category.tsx @@ -0,0 +1,24 @@ +import { CategoryForm } from '@/components/category/category-form' +import { createCategory } from '@/mutations/category' +import { useMutation } from '@tanstack/react-query' +import { useRouter } from 'expo-router' +import { Alert, View } from 'react-native' + +export default function CreateCategoryScreen() { + const router = useRouter() + const { mutateAsync } = useMutation({ + mutationFn: createCategory, + onError(error) { + Alert.alert(error.message) + }, + onSuccess() { + router.back() + }, + }) + + return ( + + + + ) +} diff --git a/apps/mobile/components/category/category-form.tsx b/apps/mobile/components/category/category-form.tsx new file mode 100644 index 00000000..f471de52 --- /dev/null +++ b/apps/mobile/components/category/category-form.tsx @@ -0,0 +1,87 @@ +import { type CategoryFormValues, zCategoryFormValues } from '@6pm/validation' +import { zodResolver } from '@hookform/resolvers/zod' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { useRef } from 'react' +import { Controller, FormProvider, useForm } from 'react-hook-form' +import { View } from 'react-native' +import type { TextInput } from 'react-native' +import { InputField } from '../form-fields/input-field' +import { SubmitButton } from '../form-fields/submit-button' +import { Tabs, TabsList, TabsTrigger } from '../ui/tabs' +import { Text } from '../ui/text' +import { SelectCategoryIconField } from './select-category-icon-field' + +type CategoryFormProps = { + onSubmit: (data: CategoryFormValues) => void + defaultValues?: CategoryFormValues +} + +export const CategoryForm = ({ + onSubmit, + defaultValues, +}: CategoryFormProps) => { + const { i18n } = useLingui() + const nameInputRef = useRef(null) + + const categoryForm = useForm({ + resolver: zodResolver(zCategoryFormValues), + defaultValues: { + name: '', + type: 'EXPENSE', + icon: 'CreditCard', + ...defaultValues, + }, + }) + + return ( + + + nameInputRef.current?.focus()} + /> + } + /> + + {t(i18n)`Type`} + ( + + + + {t(i18n)`Expense`} + + + {t(i18n)`Income`} + + + + )} + /> + + + {t(i18n)`Save`} + + + + ) +} diff --git a/apps/mobile/components/category/select-category-icon-field.tsx b/apps/mobile/components/category/select-category-icon-field.tsx new file mode 100644 index 00000000..80e59533 --- /dev/null +++ b/apps/mobile/components/category/select-category-icon-field.tsx @@ -0,0 +1,62 @@ +import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' +import { useRef } from 'react' + +import { WALLET_ICONS } from '@/lib/icons/wallet-icons' +import { useController } from 'react-hook-form' +import { Keyboard } from 'react-native' +import GenericIcon from '../common/generic-icon' +import { IconGridSheet } from '../common/icon-grid-sheet' +import { Button } from '../ui/button' + +export function SelectCategoryIconField({ + onSelect, +}: { + onSelect?: (currency: string) => void +}) { + const sheetRef = useRef(null) + const { + field: { onChange, onBlur, value }, + // fieldState, + } = useController({ name: 'icon' }) + + return ( + <> + + ( + + )} + > + { + onChange(icon) + sheetRef.current?.close() + onBlur() + onSelect?.(icon) + }} + /> + + + ) +} diff --git a/apps/mobile/mutations/category.ts b/apps/mobile/mutations/category.ts new file mode 100644 index 00000000..cfd9702b --- /dev/null +++ b/apps/mobile/mutations/category.ts @@ -0,0 +1,16 @@ +import { getHonoClient } from '@/lib/client' +import { type CategoryFormValues, CategorySchema } from '@6pm/validation' + +export async function createCategory(data: CategoryFormValues) { + const hc = await getHonoClient() + const result = await hc.v1.categories.$post({ + json: data, + }) + + if (result.ok) { + const category = CategorySchema.parse(await result.json()) + return category + } + + return result +} diff --git a/apps/mobile/mutations/wallet.ts b/apps/mobile/mutations/wallet.ts index 3745911e..02ca7060 100644 --- a/apps/mobile/mutations/wallet.ts +++ b/apps/mobile/mutations/wallet.ts @@ -26,8 +26,8 @@ export async function updateWallet({ id, data, }: { - id: string; - data: AccountFormValues; + id: string + data: AccountFormValues }) { const { balance, ...walletData } = data const hc = await getHonoClient() diff --git a/packages/validation/src/category.zod.ts b/packages/validation/src/category.zod.ts index ffbd9187..f7387472 100644 --- a/packages/validation/src/category.zod.ts +++ b/packages/validation/src/category.zod.ts @@ -3,7 +3,9 @@ import { CategoryTypeSchema } from './prisma' export const zCreateCategory = z.object({ type: CategoryTypeSchema, - name: z.string(), + name: z.string().min(1, { + message: 'Category name is required', + }), description: z.string().optional(), color: z.string().optional(), icon: z.string().optional(), @@ -12,9 +14,17 @@ export type CreateCategory = z.infer export const zUpdateCategory = z.object({ type: CategoryTypeSchema.optional(), - name: z.string().optional(), + name: z + .string() + .min(1, { + message: 'Category name is required', + }) + .optional(), description: z.string().optional(), color: z.string().optional(), icon: z.string().optional(), }) export type UpdateCategory = z.infer + +export const zCategoryFormValues = zCreateCategory +export type CategoryFormValues = z.infer