diff --git a/package.json b/package.json index 16913bc..c652b0d 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-hook-form": "^7.51.4", "react-i18next": "^12.3.1", "react-native": "0.74.5", + "react-native-element-dropdown": "^2.12.2", "react-native-flash-message": "^0.4.2", "react-native-gesture-handler": "~2.16.2", "react-native-keyboard-controller": "^1.13.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33600f1..cc37995 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: react-native: specifier: 0.74.5 version: 0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0) + react-native-element-dropdown: + specifier: ^2.12.2 + version: 2.12.2(react-native@0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) react-native-flash-message: specifier: ^0.4.2 version: 0.4.2(prop-types@15.8.1)(react-native@0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) @@ -6136,6 +6139,13 @@ packages: react-native-svg: optional: true + react-native-element-dropdown@2.12.2: + resolution: {integrity: sha512-Tf8hfRuniYEXo+LGoVgIMoItKWuPLX6jbqlwAFgMbBhmWGTuV+g1OVOAx/ny16kgnwp+NhgJoWpxhVvr7HSmXA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '*' + react-native: '*' + react-native-flash-message@0.4.2: resolution: {integrity: sha512-YvdXRW9AGMTI99S3DJZhLO0mbk/ehKv/UQf4/Df+3dtGi8DlkidRbyqCQZk1WMtZ7rN85PMTGr/xEI9CF9z0YA==} peerDependencies: @@ -15144,6 +15154,12 @@ snapshots: - '@babel/core' - supports-color + react-native-element-dropdown@2.12.2(react-native@0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): + dependencies: + lodash: 4.17.21 + react: 18.2.0 + react-native: 0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0) + react-native-flash-message@0.4.2(prop-types@15.8.1)(react-native@0.74.5(@babel/core@7.24.5)(@babel/preset-env@7.24.5(@babel/core@7.24.5))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 diff --git a/src/api/common/utils.tsx b/src/api/common/utils.tsx index 812d00a..fe24b2c 100644 --- a/src/api/common/utils.tsx +++ b/src/api/common/utils.tsx @@ -77,3 +77,43 @@ export const toSnakeCase = (obj: GenericObject): GenericObject => { } return newObj; }; + +const DEFAULT_ERROR_MESSAGE = 'something went wrong'; +const parseError = (data: any) => { + const { error, errors } = data || {}; + if (error) { + return error; + } + if (errors) { + const { fullMessages, base } = errors; + if (fullMessages) { + const [firstMessage] = fullMessages; + return firstMessage; + } else if (base) { + const [firstMessage] = base; + return firstMessage; + } else if (Array.isArray(errors)) { + const [firstMessage] = errors; + return firstMessage; + } else { + const errorKey = Object.keys(errors)[0]; + const error = errors[errorKey][0]; + return `${errorKey} ${error}`; + } + } + return DEFAULT_ERROR_MESSAGE; +}; +export const parseAxiosError = (error: any) => { + if (error) { + const { response } = error; + if (!response) { + return DEFAULT_ERROR_MESSAGE; + } + if (response.status === 500) { + return DEFAULT_ERROR_MESSAGE; + } else { + return parseError(response?.data); + } + } + return DEFAULT_ERROR_MESSAGE; +}; diff --git a/src/api/products/use-details.ts b/src/api/products/use-details.ts new file mode 100644 index 0000000..89a39a8 --- /dev/null +++ b/src/api/products/use-details.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; + +import { client, parseAxiosError } from '../common'; +import type { Product } from './types'; +export const getItemDetails = async (id: number): Promise => { + try { + const { data } = await client.get(`products/${id}`); + return data; + } catch (error) { + throw parseAxiosError(error); + } +}; +export const useGetItemDetails = (id: number) => + useQuery({ + queryKey: ['getItemDetails'], + queryFn: () => getItemDetails(id), + }); diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index e88e9f6..b6ac443 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -8,6 +8,7 @@ import { Favorite as FavoriteIcon, Home as HomeIcon, Menu as MenuIcon, + Sell as SellIcon, } from '@/ui/icons'; const renderIcon = ( @@ -28,13 +29,13 @@ const tabs = [ testID: 'feed-tab', headerShown: false, }, - // { - // name: 'product-list', - // title: 'product-list', - // icon: SellIcon, - // testID: 'product-list-tab', - // headerShown: false, - // }, + { + name: 'product-list', + title: 'Product List', + testID: 'product-list-tab', + icon: SellIcon, + headerShown: false, + }, { name: 'settings', title: 'Settings', @@ -102,6 +103,7 @@ export default function TabLayout() { renderIcon(icon, color, focused), tabBarTestID: testID, headerShown, + tabBarButton: name === 'product-list' ? () => null : undefined, }} /> ))} diff --git a/src/app/product-list.tsx b/src/app/(app)/product-list.tsx similarity index 96% rename from src/app/product-list.tsx rename to src/app/(app)/product-list.tsx index d02b37e..ccc637b 100644 --- a/src/app/product-list.tsx +++ b/src/app/(app)/product-list.tsx @@ -20,7 +20,7 @@ const FiltersButton = ({ products }: { products: Product[] }) => { return; } return ( - + Filers @@ -125,7 +125,7 @@ export default function ProductList() { - + - ); diff --git a/src/app/details/[id].tsx b/src/app/details/[id].tsx new file mode 100644 index 0000000..805039d --- /dev/null +++ b/src/app/details/[id].tsx @@ -0,0 +1,55 @@ +import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; +import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import { useGetItemDetails } from '@/api/products/use-details'; +import { AddToCartSection } from '@/components/details/add-to-cart'; +import { ImageDisplayer } from '@/components/details/image-displayer'; +import { HeaderLogo } from '@/components/header-logo'; +import { SafeAreaView, ScrollView, Text, TouchableOpacity, View } from '@/ui'; + +export default function DetailsScreen() { + const router = useRouter(); + const { id } = useLocalSearchParams(); + const { data } = useGetItemDetails(Number(id)); + const [quantity, setQuantity] = useState(1); + + return ( + + + + + + + + + {data?.state === 'totaly_new' ? 'New' : 'Restored'} + + + + X + + + {data?.title} + {data?.category.name} + {data?.unit_price} + + {}} + /> + + Product description + {data?.description} + + + + + ); +} diff --git a/src/components/details/add-to-cart.tsx b/src/components/details/add-to-cart.tsx new file mode 100644 index 0000000..01a3870 --- /dev/null +++ b/src/components/details/add-to-cart.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import { Dropdown } from 'react-native-element-dropdown'; + +import { Button, Text, View } from '@/ui'; + +type DropdownItem = { + label: string; + value: number; +}; + +type AddToCartSectionProps = { + setQuantity: React.Dispatch>; + quantity: number; + buy: () => void; +}; + +export const AddToCartSection = ({ + quantity, + setQuantity, + buy, +}: AddToCartSectionProps) => { + const numberItems: DropdownItem[] = Array.from( + { length: quantity }, + (_, index) => ({ + label: (index + 1).toString(), + value: index + 1, + }) + ); + const [selectedValue, setSelectedValue] = useState('1'); + + return ( + + + Quantity + { + setSelectedValue(item.value.toString()); + setQuantity(item.value); + }} + /> + + + Avalability: {quantity} items +