Skip to content

Commit

Permalink
feat(api): add categories bootstrap when creating user (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Aug 2, 2024
1 parent fbfb599 commit fc13e88
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 21 deletions.
85 changes: 85 additions & 0 deletions apps/api/v1/constants/category.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export const DEFAULT_INCOME_CATEGORIES = [
{
en: 'Salary',
vi: 'Tiền lương',
icon: 'BriefcaseBusiness',
},
{
en: 'Gift',
vi: 'Quà tặng',
icon: 'Clover',
},
{
en: 'Investment',
vi: 'Đầu tư',
icon: 'ChartLine',
},
{
en: 'Interest',
vi: 'Lãi suất',
icon: 'BadgePercent',
},
{
en: 'Other Income',
vi: 'Thu nhập khác',
icon: 'HandCoins',
},
]

export const DEFAULT_EXPENSE_CATEGORIES = [
{
en: 'Groceries',
vi: 'Nhu yếu phẩm',
icon: 'ShoppingBasket',
},
{
en: 'Mortgage',
vi: 'Tiền nhà',
icon: 'House',
},
{
en: 'Bills & Utilities',
vi: 'Hóa đơn & Tiện ích',
icon: 'HousePlug',
},
{
en: 'Food & Beverage',
vi: 'Thức ăn & Đồ uống',
icon: 'UtensilsCrossed',
},
{
en: 'Transportation',
vi: 'Di chuyển',
icon: 'CarFront',
},
{
en: 'Shopping',
vi: 'Mua sắm',
icon: 'ShoppingBag',
},
{
en: 'Health',
vi: 'Sức khỏe',
icon: 'HeartPulse',
},
{
en: 'Entertainment',
vi: 'Giải trí',
icon: 'Drama',
},
{
en: 'Education',
vi: 'Giáo dục',
icon: 'BookHeart',
},
{
en: 'Investment',
vi: 'Đầu tư',
icon: 'ChartLine',
},
{
en: 'Other Expense',
vi: 'Chi phí khác',
icon: 'Coins',
},
]
10 changes: 9 additions & 1 deletion apps/api/v1/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { zCreateUser } from '@6pm/validation'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { getAuthUser } from '../middlewares/auth'
import { bootstrapUserDefaultCategories } from '../services/category.service'
import { createUser } from '../services/user.service'
import { zDeviceLanguageHeader } from './utils'

const router = new Hono().post(
'/',
zValidator('json', zCreateUser),
zDeviceLanguageHeader,
async (c) => {
const existingUser = getAuthUser(c)
const deviceLanguage = c.req.valid('header')['x-device-language']

if (existingUser) {
return c.json({ message: 'user already exists' }, 409)
Expand All @@ -18,7 +22,11 @@ const router = new Hono().post(
const data = c.req.valid('json')

try {
const user = await createUser({ ...data, id: userId })
const user = await createUser({ data: { ...data, id: userId } })

// bootstrap user data
await bootstrapUserDefaultCategories({ user, language: deviceLanguage })

return c.json(user, 201)
} catch (e) {
return c.json({ userId, message: 'failed to create user', cause: e }, 500)
Expand Down
9 changes: 9 additions & 0 deletions apps/api/v1/routes/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

export const zDeviceLanguageHeader = zValidator(
'header',
z.object({
'x-device-language': z.string().optional(),
}),
)
3 changes: 2 additions & 1 deletion apps/api/v1/services/budget-invitation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ export async function respondToBudgetInvitation({

// Create find or new user
const user =
(await findUserByEmail(userData.email)) || (await createUser(userData))
(await findUserByEmail(userData.email)) ||
(await createUser({ data: userData }))

// Create invitation response
const acceptedAt = new Date()
Expand Down
32 changes: 31 additions & 1 deletion apps/api/v1/services/category.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { CreateCategory, UpdateCategory } from '@6pm/validation'
import type { Category, User } from '@prisma/client'
import { type Category, CategoryType, type User } from '@prisma/client'
import prisma from '../../lib/prisma'
import {
DEFAULT_EXPENSE_CATEGORIES,
DEFAULT_INCOME_CATEGORIES,
} from '../constants/category.const'

export async function canUserCreateCategory({
// biome-ignore lint/correctness/noUnusedVariables: <explanation>
Expand Down Expand Up @@ -117,3 +121,29 @@ export async function findCategoriesOfUser({
where: { userId: user.id },
})
}

export async function bootstrapUserDefaultCategories({
user,
language = 'en',
}: { user: User; language?: string }) {
const defaultCategories = [
...DEFAULT_INCOME_CATEGORIES.map((c) => ({
...c,
type: CategoryType.INCOME,
})),
...DEFAULT_EXPENSE_CATEGORIES.map((c) => ({
...c,
type: CategoryType.EXPENSE,
})),
].map(({ en, vi, ...category }) => ({
...category,
name: language === 'vi' ? vi : en,
}))

return prisma.category.createMany({
data: defaultCategories.map((category) => ({
...category,
userId: user.id,
})),
})
}
2 changes: 1 addition & 1 deletion apps/api/v1/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function findUserByEmail(email: string) {
})
}

export async function createUser(data: CreateUser) {
export async function createUser({ data }: { data: CreateUser }) {
return await prisma.user.create({
data,
})
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/app/(app)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useLocale } from '@/locales/provider'
import { useAuth } from '@clerk/clerk-expo'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { useQueryClient } from '@tanstack/react-query'
import { LinearGradient } from 'expo-linear-gradient'
import { Link } from 'expo-router'
import {
Expand All @@ -38,6 +40,7 @@ export default function SettingsScreen() {
const { i18n } = useLingui()
const { language } = useLocale()
const { colorScheme } = useColorScheme()
const queryClient = useQueryClient()

return (
<View className="bg-card">
Expand Down Expand Up @@ -185,7 +188,11 @@ export default function SettingsScreen() {
{
text: t(i18n)`Sign out`,
style: 'destructive',
onPress: () => signOut(),
onPress: async () => {
await AsyncStorage.clear()
queryClient.clear()
await signOut()
},
},
])
}
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/components/common/generic-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const GenericIcon: FC<
> = ({ name, ...props }) => {
const LucideIcon = icons[name]

if (!LucideIcon) {
console.error(`Icon "${name}" not found`)
return null
}

return <LucideIcon {...props} />
}

Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/lib/icons/category-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const CATEGORY_EXPENSE_ICONS: Array<keyof typeof icons> = [
'Lamp',
'BedDouble',
'Drill',
'Home',
'House',
'Refrigerator',
'Cat',
'Bird',
Expand Down Expand Up @@ -63,10 +63,10 @@ export const CATEGORY_INCOME_ICONS: Array<keyof typeof icons> = [
'Handshake',
'PiggyBank',
'SmartphoneNfc',
'BadgeCent',
'BadgePercent',
'Trophy',
'Clover',
'LineChart',
'ChartLine',
'Store',
'BriefcaseBusiness',
'Building2',
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"expo-web-browser": "~13.0.3",
"hono": "^4.4.8",
"lodash-es": "^4.17.21",
"lucide-react-native": "^0.390.0",
"lucide-react-native": "^0.417.0",
"nativewind": "^4.0.36",
"posthog-react-native": "^3.1.1",
"react": "18.3.1",
Expand Down
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fc13e88

Please sign in to comment.