Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product detail Screen #9

Merged
merged 9 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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];
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 }}
/>
</>
);
};
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
Loading