519 lines
16 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import {
View,
Text,
TouchableOpacity,
ScrollView,
Image,
Dimensions,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './providerDetailsScreen.styles';
import { useAppTheme } from '@theme';
import {
addToCartThunk,
updateCartItemThunk,
} from '../../../store/commonreducers/cart';
import { AppStackParamList } from '../../../navigation/appStack';
import { useAppDispatch, useAppSelector } from '@store';
import { getProductDetailsThunk } from './thunk';
import { getDiscountPercentage, formatPrice } from '@components';
type ProviderDetailsNavProp = StackNavigationProp<
AppStackParamList,
'ProviderDetailsScreen'
>;
type ProviderDetailsRouteProp = RouteProp<
AppStackParamList,
'ProviderDetailsScreen'
>;
const { width } = Dimensions.get('window');
const BASE_URL = 'https://2e1b-202-8-116-13.ngrok-free.app';
const getFullUrl = (url?: string) => {
if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
};
// TODO: move these into your shared Product type once the backend
// response includes them.
interface ProductReview {
id: string;
userName?: string;
rating: number;
comment?: string;
createdAt?: string;
}
type RatingBreakdown = Partial<Record<1 | 2 | 3 | 4 | 5, number>>;
export const ProviderDetailsScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation<ProviderDetailsNavProp>();
const route = useRoute<ProviderDetailsRouteProp>();
// Notice we still use providerId param name to avoid breaking routing everywhere
const { providerId } = route.params;
const cartItems = useAppSelector(state => state.cart.items);
const { product, isLoading } = useAppSelector(state => state.providerDetails);
// Find this product in the cart (if already added)
const cartItem = product
? cartItems.find(i => i.productId === product.id)
: undefined;
const isInCart = !!cartItem;
const [activeImageIndex, setActiveImageIndex] = useState(0);
const [localQty, setLocalQty] = useState(0);
useEffect(() => {
if (providerId) {
dispatch(getProductDetailsThunk(providerId));
}
}, [providerId, dispatch]);
// Sync localQty with cart quantity when cart loads or product changes
useEffect(() => {
if (cartItem) {
setLocalQty(cartItem.quantity);
} else {
setLocalQty(0);
}
}, [cartItem?.quantity, cartItem?.productId]);
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const slide = Math.round(e.nativeEvent.contentOffset.x / width);
setActiveImageIndex(slide);
};
const handleAddToCart = () => {
if (!product) return;
dispatch(
addToCartThunk({
productId: product.id,
quantity: Math.max(1, localQty), // always add at least 1
}),
);
};
const handleIncrement = () => {
if (!product) return;
const newQty = localQty + 1;
setLocalQty(newQty);
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
};
const handleDecrement = () => {
if (!product || localQty <= 1) return;
const newQty = localQty - 1;
setLocalQty(newQty);
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
};
const handleRateProduct = () => {
navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' });
};
const renderStars = (value: number, size = 14) => {
const rounded = Math.round(value);
const starColor = (colors as any).warning ?? '#F5A623';
return (
<View style={styles.starsRow}>
{[1, 2, 3, 4, 5].map(i => (
<Text
key={i}
style={[styles.starIcon, { fontSize: size, color: starColor }]}
>
{i <= rounded ? '★' : '☆'}
</Text>
))}
</View>
);
};
const renderImages = () => {
if (isLoading) {
return (
<View style={styles.carouselWrap}>
<View
style={[
styles.fallbackIconWrap,
{ backgroundColor: (colors as any).surface ?? colors.cardBg },
]}
/>
</View>
);
}
const images = product?.media?.length
? product.media
: [
{
id: 'fallback',
url: product?.imageUrl || '',
mediaType: 'IMAGE',
sortOrder: 0,
productId: '',
createdAt: '',
},
];
return (
<View style={styles.carouselWrap}>
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={handleScroll}
>
{images.map(img => (
<Image
key={img.id}
source={{ uri: getFullUrl(img.url) }}
style={styles.carouselImage}
/>
))}
</ScrollView>
{images.length > 1 && (
<View style={styles.paginationDots}>
{images.map((_, i) => (
<View
key={i}
style={[styles.dot, i === activeImageIndex && styles.dotActive]}
/>
))}
</View>
)}
</View>
);
};
const renderProductInfo = () => {
if (isLoading || !product) {
return (
<View style={styles.infoBox}>
<View style={styles.skeletonTitle} />
<View style={styles.skeletonPrice} />
<View style={styles.skeletonText} />
<View style={styles.skeletonText} />
</View>
);
}
const discount = getDiscountPercentage(
product.price,
product.compareAtPrice,
);
const currency = product.currency === 'INR' ? '₹' : product.currency;
return (
<View style={styles.infoBox}>
<View style={styles.brandRow}>
<Text style={styles.brandText}>
{product.brand || product.merchant?.name || 'BRAND'}
</Text>
<View style={styles.ratingBadge}>
<Text style={{ fontSize: 11 }}></Text>
<Text style={styles.ratingBadgeText}>
{parseFloat(product.avgRating).toFixed(1)}
</Text>
</View>
</View>
<Text style={styles.titleText}>{product.name}</Text>
<View style={styles.priceRow}>
<Text style={styles.priceText}>
{formatPrice(product.price, currency)}
</Text>
{product.compareAtPrice &&
parseFloat(product.compareAtPrice) > parseFloat(product.price) && (
<Text style={styles.comparePriceText}>
{formatPrice(product.compareAtPrice, currency)}
</Text>
)}
{discount && (
<View style={styles.discountBadgeWrap}>
<Text style={styles.discountBadgeText}>{discount}</Text>
</View>
)}
</View>
<Text style={styles.taxText}>Inclusive of all taxes</Text>
</View>
);
};
const renderRatingsSection = () => {
if (isLoading) {
return (
<View style={styles.ratingsSection}>
<View style={styles.skeletonBlock} />
<View style={[styles.skeletonBlock, { height: 80 }]} />
</View>
);
}
const avgRating = product?.avgRating ? parseFloat(product.avgRating) : 0;
const reviews: ProductReview[] = (product as any)?.reviews ?? [];
const totalRatings: number =
(product as any)?.ratingsCount ?? reviews.length ?? 0;
const breakdown: RatingBreakdown | undefined = (product as any)
?.ratingBreakdown;
const hasRatings = totalRatings > 0;
return (
<View style={styles.ratingsSection}>
<View style={styles.ratingsHeaderRow}>
<Text style={styles.sectionTitle}>Ratings & Reviews</Text>
{reviews.length > 3 && (
<TouchableOpacity activeOpacity={0.7}>
<Text style={styles.seeAllText}>See all</Text>
</TouchableOpacity>
)}
</View>
{hasRatings ? (
<View style={styles.ratingsSummaryCard}>
<View
style={[
styles.ratingsSummaryLeft,
breakdown && styles.ratingsSummaryLeftWithDivider,
]}
>
<Text style={styles.avgRatingNumber}>{avgRating.toFixed(1)}</Text>
{renderStars(avgRating, 16)}
<Text style={styles.totalRatingsText}>
{totalRatings} {totalRatings === 1 ? 'rating' : 'ratings'}
</Text>
</View>
{breakdown && (
<View style={styles.ratingsBarsWrap}>
{[5, 4, 3, 2, 1].map(star => {
const count = breakdown[star as 1 | 2 | 3 | 4 | 5] ?? 0;
const pct =
totalRatings > 0 ? (count / totalRatings) * 100 : 0;
return (
<View key={star} style={styles.barRow}>
<Text style={styles.barLabel}>{star}</Text>
<View style={styles.barTrack}>
<View style={[styles.barFill, { width: `${pct}%` }]} />
</View>
</View>
);
})}
</View>
)}
</View>
) : (
<View style={styles.emptyRatingsCard}>
<Text style={styles.emptyRatingsIcon}></Text>
<Text style={styles.emptyRatingsTitle}>No ratings yet</Text>
<Text style={styles.emptyRatingsSubtitle}>
Be the first to share what you think of this product.
</Text>
<TouchableOpacity
style={styles.rateProductBtn}
activeOpacity={0.8}
onPress={handleRateProduct}
>
<Text style={styles.rateProductBtnText}>Rate this product</Text>
</TouchableOpacity>
</View>
)}
{reviews.length > 0 && (
<View style={styles.reviewsList}>
{reviews.slice(0, 3).map(review => (
<View key={review.id} style={styles.reviewCard}>
<View style={styles.reviewCardHeader}>
<View style={styles.reviewerAvatar}>
<Text style={styles.reviewerAvatarText}>
{(review.userName || 'U').charAt(0).toUpperCase()}
</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.reviewerName}>
{review.userName || 'Anonymous'}
</Text>
{renderStars(review.rating, 12)}
</View>
<Text style={styles.reviewDate}>
{review.createdAt
? new Date(review.createdAt).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
})
: ''}
</Text>
</View>
{!!review.comment && (
<Text style={styles.reviewComment}>{review.comment}</Text>
)}
</View>
))}
</View>
)}
</View>
);
};
return (
<View style={styles.container}>
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentBody}
showsVerticalScrollIndicator={false}
>
<View style={{ position: 'relative' }}>
{renderImages()}
<View style={styles.topIconRow}>
<TouchableOpacity
style={styles.iconButton}
activeOpacity={0.8}
onPress={() => navigation.goBack()}
>
<Text style={styles.iconButtonText}></Text>
</TouchableOpacity>
<View style={styles.iconButtonGroup}>
<TouchableOpacity
style={[styles.iconButton, { marginRight: 12 }]}
activeOpacity={0.8}
>
<Text style={styles.iconButtonText}>🔗</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.iconButton} activeOpacity={0.8}>
<Text style={{ fontSize: 17 }}>🤍</Text>
</TouchableOpacity>
</View>
</View>
</View>
{renderProductInfo()}
<View style={styles.sectionDivider} />
<View style={styles.detailsSection}>
<Text style={styles.sectionTitle}>Product Details</Text>
<Text style={styles.descriptionText}>
{product?.description ||
'No description available for this product.'}
</Text>
<View style={styles.metaGrid}>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Category</Text>
<Text style={styles.metaValue}>
{product?.category?.name || 'N/A'}
</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>SKU</Text>
<Text style={styles.metaValue}>{product?.sku || 'N/A'}</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Stock</Text>
<Text style={styles.metaValue}>
{product?.stockQuantity ? 'In Stock' : 'Out of Stock'}
</Text>
</View>
<View style={styles.metaItem}>
<Text style={styles.metaLabel}>Merchant</Text>
<Text style={styles.metaValue}>
{product?.merchant?.name || 'N/A'}
</Text>
</View>
</View>
</View>
<View style={styles.sectionDivider} />
{renderRatingsSection()}
</ScrollView>
{/* Sticky Bottom Bar */}
<View style={styles.bottomBarWrap}>
{isInCart ? (
// ── Already in cart: show inline qty stepper + Go to Cart ──
<>
<View style={styles.qtySelector}>
<TouchableOpacity
style={styles.qtyBtn}
onPress={handleDecrement}
activeOpacity={0.7}
disabled={localQty <= 1}
>
<Text style={styles.qtyBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.qtyValue}>{localQty}</Text>
<TouchableOpacity
style={styles.qtyBtn}
onPress={handleIncrement}
activeOpacity={0.7}
>
<Text style={styles.qtyBtnText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.addBtn}
activeOpacity={0.8}
onPress={() => navigation.navigate('CartScreen')}
>
<Text style={styles.addBtnText}>Go to Cart</Text>
</TouchableOpacity>
</>
) : (
// ── Not in cart: qty picker + Add to Cart ──
<>
<View style={styles.qtySelector}>
<TouchableOpacity
style={styles.qtyBtn}
onPress={() => setLocalQty(q => Math.max(0, q - 1))}
activeOpacity={0.7}
disabled={localQty <= 0}
>
<Text style={styles.qtyBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.qtyValue}>{localQty}</Text>
<TouchableOpacity
style={styles.qtyBtn}
onPress={() => setLocalQty(q => q + 1)}
activeOpacity={0.7}
>
<Text style={styles.qtyBtnText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.addBtn}
activeOpacity={0.8}
onPress={handleAddToCart}
disabled={
!product ||
(product.isTrackStock && product.stockQuantity === 0)
}
>
<Text style={styles.addBtnText}>
{!product
? 'Loading...'
: product.isTrackStock && product.stockQuantity === 0
? 'Out of Stock'
: 'Add to Cart'}
</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
);
};