Skip to content

Commit

Permalink
Merge pull request #9 from SofiaCantero24/product-detail
Browse files Browse the repository at this point in the history
Product detail Screen
  • Loading branch information
SofiaCantero24 authored Nov 11, 2024
2 parents a6a5645 + b37e76a commit a89ef74
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 20 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

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

40 changes: 40 additions & 0 deletions src/api/common/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Check warning on line 100 in src/api/common/utils.tsx

View workflow job for this annotation

GitHub Actions / Lint TS (eslint, prettier)

'error' is already declared in the upper scope on line 83 column 11
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;
};
17 changes: 17 additions & 0 deletions src/api/products/use-details.ts
Original file line number Diff line number Diff line change
@@ -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<Product> => {
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),
});
16 changes: 9 additions & 7 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Favorite as FavoriteIcon,
Home as HomeIcon,
Menu as MenuIcon,
Sell as SellIcon,
} from '@/ui/icons';

const renderIcon = (
Expand All @@ -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',
Expand Down Expand Up @@ -102,6 +103,7 @@ export default function TabLayout() {
renderIcon(icon, color, focused),
tabBarTestID: testID,
headerShown,
tabBarButton: name === 'product-list' ? () => null : undefined,
}}
/>
))}
Expand Down
4 changes: 2 additions & 2 deletions src/app/product-list.tsx → src/app/(app)/product-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const FiltersButton = ({ products }: { products: Product[] }) => {
return;
}
return (
<View className="absolute -bottom-8 flex w-auto items-center justify-center self-center pb-20">
<View className="absolute bottom-24 flex w-auto items-center justify-center self-center pb-20">
<TouchableOpacity className="flex-row items-center gap-4 rounded-full bg-dark_violet p-4 px-8">
<Text className="text-lg font-bold text-white">Filers</Text>
<Image className="h-4 w-4" source={images.filterIcon()} />
Expand Down Expand Up @@ -125,7 +125,7 @@ export default function ProductList() {
</TouchableOpacity>
<SearchBar setQuery={setQuery} query={query} />
<SearchResult query={query} clearQuery={clearQuery} />
<View className={`${query === '' ? 'pb-56' : 'pb-80'}`}>
<View className={query === '' ? 'pb-52' : 'pb-72'}>
<ProductsList
products={productsToDisplay}
onEndReached={handleLoadMore}
Expand Down
1 change: 0 additions & 1 deletion src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function RootLayoutNav() {
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="signup" options={{ headerShown: false }} />
<Stack.Screen name="product-list" options={{ headerShown: false }} />
</Stack>
</Providers>
);
Expand Down
55 changes: 55 additions & 0 deletions src/app/details/[id].tsx
Original file line number Diff line number Diff line change
@@ -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<number>(1);

return (
<SafeAreaView className="flex-1 bg-gray-100">
<Stack.Screen options={{ headerShown: false }} />
<ScrollView>
<HeaderLogo />
<View className="px-4 py-3">
<View className="flex-row justify-between">
<View
className={twMerge(
data?.state === 'totaly_new' ? 'bg-new' : 'bg-restored',
'h-6 w-1/5 items-center justify-center rounded-md'
)}
>
<Text className="text-md text-white">
{data?.state === 'totaly_new' ? 'New' : 'Restored'}
</Text>
</View>
<TouchableOpacity className="mr-4" onPress={router.back}>
<Text className="text-2xl font-bold text-red-700">X</Text>
</TouchableOpacity>
</View>
<Text className="text-2xl font-semibold">{data?.title}</Text>
<Text className="text-2xl font-light">{data?.category.name}</Text>
<Text className="font-semi-bold font-">{data?.unit_price}</Text>
<ImageDisplayer images={data?.pictures} />
<AddToCartSection
quantity={quantity}
setQuantity={setQuantity}
buy={() => {}}
/>
<View className="mb-4">
<Text className="mb-3 mr-2 font-bold">Product description</Text>
<Text>{data?.description}</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
67 changes: 67 additions & 0 deletions src/components/details/add-to-cart.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<number>>;
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<string>('1');

return (
<View className="mb-4 flex-row items-center justify-between">
<View className="items-center">
<Text className="mb-3 mr-2 font-bold">Quantity</Text>
<Dropdown
style={{
width: 90,
borderColor: 'black',
borderWidth: 2,
padding: 8,
borderRadius: 8,
height: 43,
marginBottom: 8,
}}
data={numberItems}
labelField="label"
valueField="value"
placeholder="1"
value={selectedValue}
onChange={(item: DropdownItem) => {
setSelectedValue(item.value.toString());
setQuantity(item.value);
}}
/>
</View>
<View className="w-4/5 items-center">
<Text className=" mr-2 font-bold">Avalability: {quantity} items</Text>
<Button
className="mt-3 h-12 w-72"
label="Add to cart"
textClassName="font-bold text-base"
onPress={buy}
/>
</View>
</View>
);
};
47 changes: 47 additions & 0 deletions src/components/details/image-displayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react';
import { FlatList } from 'react-native';

import { Image, TouchableOpacity, View } from '@/ui';

export const ImageDisplayer = ({
images,
}: {
images: string[] | undefined;
}) => {
const [selectedImage, setSelectedImage] = useState(0);

if (images === undefined) {
return;
}

return (
<>
<View className="h-68 w-68 my-3 w-full rounded-lg">
<Image
source={{ uri: images[selectedImage] }}
className="h-72 w-full"
contentFit="contain"
/>
</View>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={images}
keyExtractor={(index) => index.toString()}
renderItem={({ item, index }) => (
<TouchableOpacity
className="mr-4 h-28 w-28 rounded-md"
onPress={() => setSelectedImage(index)}
>
<Image
source={{ uri: item }}
contentFit="contain"
className="h-28 w-28 rounded-md"
/>
</TouchableOpacity>
)}
contentContainerStyle={{ paddingBottom: 16 }}

Check warning on line 43 in src/components/details/image-displayer.tsx

View workflow job for this annotation

GitHub Actions / Lint TS (eslint, prettier)

Inline style: { paddingBottom: 16 }
/>
</>
);
};
10 changes: 8 additions & 2 deletions src/components/home/carousel-item.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Link } from 'expo-router';
import { twMerge } from 'tailwind-merge';

import type { Product } from '@/api/products/types';
Expand All @@ -22,7 +23,12 @@ const Icon = ({ focused, ...props }: IconType) => {
};
export const CarouselItem = ({ item }: { item: Product }) => {
return (
<>
<Link
href={{
pathname: '/details/[id]',
params: { id: item.id },
}}
>
<View className="h-64 w-48 rounded-lg bg-white shadow shadow-gray-600">
<View className="items-center">
<Image
Expand Down Expand Up @@ -52,6 +58,6 @@ export const CarouselItem = ({ item }: { item: Product }) => {
<Icon focused={false} />
</View>
</View>
</>
</Link>
);
};
2 changes: 1 addition & 1 deletion src/components/home/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const HorizontalCarousel = () => {
<TouchableOpacity
className="items-center"
onPress={() => {
router.push('/product-list');
router.push('/(app)/product-list');
}}
>
<Text className="font-semibold text-link">See all</Text>
Expand Down
Loading

0 comments on commit a89ef74

Please sign in to comment.