import React, { useState, useMemo } from 'react'; import { View, ScrollView, Alert, Dimensions, FlatList, RefreshControl } from 'react-native'; import { useRouter } from 'expo-router'; import { ImageCarousel, tw, BottomDialog, useManualRefresh, useMarkDataFetchers, LoadingDialog, ImageUploader, MyText, MyTextInput, MyTouchableOpacity, Quantifier } from 'common-ui'; import { LinearGradient } from 'expo-linear-gradient'; import usePickImage from 'common-ui/src/components/use-pick-image'; import { theme } from 'common-ui/src/theme'; import dayjs from 'dayjs'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import FontAwesome5 from '@expo/vector-icons/FontAwesome5'; import { trpc, trpcClient } from '@/src/trpc-client'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useFlashNavigationStore } from '@/components/stores/flashNavigationStore'; import FloatingCartBar from './floating-cart-bar'; const { width: screenWidth } = Dimensions.get("window"); const carouselWidth = screenWidth; const carouselHeight = carouselWidth * 0.8; const extractKeyFromUrl = (url: string): string => { const u = new URL(url); const rawKey = u.pathname.replace(/^\/+/, ""); return decodeURIComponent(rawKey); }; interface ProductDetailProps { productId: string; isFlashDelivery?: boolean; } const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => { if (unit?.toLowerCase() === 'kg' && quantity < 1) { return { value: `${Math.round(quantity * 1000)} g`, display: `${Math.round(quantity * 1000)}g` }; } return { value: `${quantity} ${unit}(s)`, display: `${quantity}${unit}` }; }; const ProductDetail: React.FC = ({ productId, isFlashDelivery = false }) => { const router = useRouter(); const [showAllSlots, setShowAllSlots] = useState(false); const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false); const [reviews, setReviews] = useState([]); const [reviewsLoading, setReviewsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [reviewsOffset, setReviewsOffset] = useState(0); const [refreshing, setRefreshing] = useState(false); const { data: productDetail, isLoading, error, refetch } = trpc.user.product.getProductDetails.useQuery({ id: productId }); const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular"; const { addToCart = () => {} } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, cartType) || {}; const { addToCart:addTOFlashCart = () => {} } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, 'flash') || {}; const cartData = useGetCart({}, cartType); const updateCartItem = useUpdateCartItem({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, cartType); const removeFromCart = useRemoveFromCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, cartType); const { getQuickestSlot } = useProductSlotIdentifier(); const { setShouldNavigateToCart } = useFlashNavigationStore(); // Find current quantity from cart data const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null; const quantity = cartItem?.quantity || 0; const handleQuantityChange = (newQuantity: number) => { if (!productDetail) return; if (newQuantity === 0 && cartItem) { removeFromCart.mutate({ itemId: cartItem.id }); } else if (newQuantity === 1 && !cartItem) { handleAddToCart(productDetail.id); } else if (cartItem) { updateCartItem.mutate({ itemId: cartItem.id, quantity: newQuantity }); } }; useManualRefresh(() => { refetch(); }); useMarkDataFetchers(() => { refetch(); }); const handleAddToCart = (productId: number) => { if (isFlashDelivery) { if (!productDetail?.isFlashAvailable) { Alert.alert("Error", "This product is not available for flash delivery"); return; } setIsLoadingDialogOpen(true); addToCart(productId, 1, 0, () => setIsLoadingDialogOpen(false)); } else { const slotId = getQuickestSlot(productId); if (!slotId) { Alert.alert("Error", "No available delivery slot for this product"); return; } setIsLoadingDialogOpen(true); addToCart(productId, 1, slotId, () => setIsLoadingDialogOpen(false)); } }; const handleBuyNow = (productId: number) => { if (isFlashDelivery) { if (!productDetail?.isFlashAvailable) { Alert.alert("Error", "This product is not available for flash delivery"); return; } setIsLoadingDialogOpen(true); addToCart(productId, 1, 0, () => setIsLoadingDialogOpen(false)); router.push(`/(drawer)/(tabs)/flash-delivery/(cart)/cart?select=${productId}`); } else { addTOFlashCart(productId, 1, 0); setShouldNavigateToCart(true); router.push('/(drawer)/(tabs)/flash-delivery'); } }; const handleSlotAddToCart = (productId: number, selectedSlotId: number) => { const cartItem = cartData.data?.items?.find((item: any) => item.productId === productId); setIsLoadingDialogOpen(true); if (cartItem) { removeFromCart.mutate( { itemId: cartItem.id }, { onSuccess: () => { addToCart(productId, cartItem.quantity + 1, selectedSlotId, () => setIsLoadingDialogOpen(false)); }, onError: () => setIsLoadingDialogOpen(false), } ); } else { addToCart(productId, 1, selectedSlotId, () => setIsLoadingDialogOpen(false)); } }; const discountPercentage = productDetail?.marketPrice ? Math.round(((Number(productDetail.marketPrice) - Number(productDetail.price)) / Number(productDetail.marketPrice)) * 100) : 0; const loadReviews = async (reset = false) => { if (reviewsLoading || (!hasMore && !reset)) return; setReviewsLoading(true); try { const { reviews: newReviews, hasMore: newHasMore } = await trpcClient.user.product.getProductReviews.query({ productId: Number(productId), limit: 10, offset: reset ? 0 : reviewsOffset, }); setReviews(reset ? newReviews : [...reviews, ...newReviews]); setHasMore(newHasMore); setReviewsOffset(reset ? 10 : reviewsOffset + 10); } catch (error) { console.error('Error loading reviews:', error); } finally { setReviewsLoading(false); } }; const onRefresh = async () => { setRefreshing(true); await refetch(); // Refetch product details await loadReviews(true); // Reset and reload reviews setRefreshing(false); }; const handleReviewSubmitted = () => { loadReviews(true); }; React.useEffect(() => { if (productDetail?.id) { loadReviews(true); } }, [productDetail?.id]); if (isLoading) { return ( Loading product details... ); } if (error || !productDetail) { return ( Oops! Product not found or error loading ); } return ( {/* */} } > {/* Image Carousel */} {/* Product Info */} {productDetail.name} {productDetail.isFlashAvailable && ( Flash Delivery )} {productDetail.isOutOfStock && ( Out of Stock )} {productDetail.shortDescription} {/* Main price display - always show regular price prominently */} ₹{productDetail.price} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display} {/* Show market price discount if available */} {productDetail.marketPrice && ( ₹{productDetail.marketPrice} {discountPercentage}% OFF )} {/* Flash price on separate line - smaller and less prominent */} {productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && ( Flash Delivery: ₹{productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display} )} {/* Action Buttons */} {quantity > 0 ? ( // Show quantifier when item is in cart ) : ( // Show "Add to Cart" button when not in cart !(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) && handleAddToCart(productDetail.id)} disabled={productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)} > {(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'} )} {isFlashDelivery ? ( !(productDetail.isOutOfStock || !productDetail.isFlashAvailable) && handleBuyNow(productDetail.id)} disabled={productDetail.isOutOfStock || !productDetail.isFlashAvailable} > {productDetail.isOutOfStock ? 'Out of Stock' : (!productDetail.isFlashAvailable ? 'Not Flash Eligible' : 'Buy Now')} ) : productDetail.isFlashAvailable ? ( productDetail.deliverySlots.length > 0 && handleBuyNow(productDetail.id)} disabled={productDetail.deliverySlots.length === 0} > {productDetail.deliverySlots.length === 0 ? 'No Slots' : 'Buy Now'} ) : ( )} {/* Delivery Slots */} Available Slots {productDetail.deliverySlots.length === 0 ? ( No delivery slots available currently ) : ( <> {productDetail.deliverySlots.slice(0, 2).map((slot, index) => ( handleSlotAddToCart(productDetail.id, slot.id)} disabled={productDetail.isOutOfStock} activeOpacity={0.7} > {dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A')} Orders Close: {dayjs(slot.freezeTime).format('h:mm A')} ))} {productDetail.deliverySlots.length > 2 && ( setShowAllSlots(true)} style={tw`items-center py-2`} > View All {productDetail.deliverySlots.length} Slots )} )} {/* Description */} About the Product {!productDetail.longDescription ? ( No detailed description available. ) : ( {productDetail.longDescription} )} {productDetail.store && ( Sourced From {productDetail.store.name} )} {/* Package Deals */} {productDetail.specialDeals && productDetail.specialDeals.length > 0 && ( Bulk Savings {productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => ( Buy {deal.quantity} {formatQuantity(parseFloat(deal.quantity), productDetail.unitNotation).display} ₹{deal.price} ))} )} {/* Reviews Section */} {/* Review Form - Moved above or keep below? Usually users want to read reviews first, but if few reviews, writing is good. The original had form then reviews. I will keep format but make it nicer. */} Customer Reviews What others are saying item.id.toString()} renderItem={({ item }) => ( {/* User Avatar Placeholder */} {item.userName ? item.userName.charAt(0).toUpperCase() : 'U'} {item.userName} {dayjs(item.reviewTime).format('MMM DD, YYYY')} {[1, 2, 3, 4, 5].map((star) => ( ))} {item.reviewBody} {item.signedImageUrls && item.signedImageUrls.length > 0 && ( {/* Using ImageCarousel for simplicity if it handles list, but it seems to be a slideshow. Original code used ImageCarousel. Let's make it more "gallery" like if possible or stick to carousel if it's robust. Actually, a row of thumbnails is better for reviews. But to avoid breaking changes if ImageCarousel is needed for signed URLs logic (headers etc), I will trust ImageCarousel or just map standard Images if standard urls. The original code used ImageCarousel. I'll stick to it to ensure images load correctly. */} )} {item.adminResponse && ( Seller Response {item.adminResponse} {item.signedAdminImageUrls && item.signedAdminImageUrls.length > 0 && ( )} )} )} onEndReached={() => loadReviews()} onEndReachedThreshold={0.5} ListEmptyComponent={ No reviews yet. Be the first to share your thoughts! } ListFooterComponent={reviewsLoading ? Loading more reviews... : null} scrollEnabled={false} /> {/* All Slots Dialog */} setShowAllSlots(false)}> All Delivery Slots {productDetail.deliverySlots.map((slot, index) => ( handleSlotAddToCart(productDetail.id, slot.id)} disabled={productDetail.isOutOfStock} activeOpacity={0.7} > {dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A')} Orders Close: {dayjs(slot.freezeTime).format('h:mm A')} ))} setShowAllSlots(false)} > Close ); }; interface ReviewFormProps { productId: number; onReviewSubmitted?: () => void; } const ReviewForm = ({ productId, onReviewSubmitted }: ReviewFormProps) => { const [reviewBody, setReviewBody] = useState(''); const [ratings, setRatings] = useState(0); const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]); const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]); const createReview = trpc.user.product.createReview.useMutation(); const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation(); const handleImagePick = usePickImage({ setFile: async (assets: any) => { if (!assets || (Array.isArray(assets) && assets.length === 0)) { setSelectedImages([]); setDisplayImages([]); return; } const files = Array.isArray(assets) ? assets : [assets]; const blobPromises = files.map(async (asset) => { const response = await fetch(asset.uri); const blob = await response.blob(); return { blob, mimeType: asset.mimeType || 'image/jpeg' }; }); const blobArray = await Promise.all(blobPromises); setSelectedImages(blobArray); setDisplayImages(files.map(asset => ({ uri: asset.uri }))); }, multiple: true, }); const handleRemoveImage = (uri: string) => { const index = displayImages.findIndex(img => img.uri === uri); if (index !== -1) { const newDisplay = displayImages.filter((_, i) => i !== index); const newFiles = selectedImages.filter((_, i) => i !== index); setDisplayImages(newDisplay); setSelectedImages(newFiles); } }; const handleSubmit = async () => { if (!reviewBody.trim() || ratings === 0) { Alert.alert('Error', 'Please provide a review and rating.'); return; } try { // Generate upload URLs const mimeTypes = selectedImages.map(s => s.mimeType); const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({ contextString: 'review', mimeTypes, }); const keys = generatedUrls.map(extractKeyFromUrl); // Upload images for (let i = 0; i < generatedUrls.length; i++) { const uploadUrl = generatedUrls[i]; const key = keys[i]; const { blob, mimeType } = selectedImages[i]; const uploadResponse = await fetch(uploadUrl, { method: 'PUT', body: blob, headers: { 'Content-Type': mimeType, }, }); if (!uploadResponse.ok) { throw new Error(`Upload failed with status ${uploadResponse.status}`); } } // Submit review with image URLs await createReview.mutateAsync({ productId, reviewBody, ratings, imageUrls: keys, uploadUrls: generatedUrls, }); Alert.alert('Success', 'Review submitted!'); onReviewSubmitted?.(); // Reset form setReviewBody(''); setRatings(0); setSelectedImages([]); setDisplayImages([]); } catch (error:any) { Alert.alert('Error', error.message || 'Failed to submit review.'); } }; return ( Rate this Product Share your experience with others {/* Rating */} {[1, 2, 3, 4, 5].map((star) => ( setRatings(star)} activeOpacity={0.7}> ))} {ratings === 0 ? 'Tap to Rate' : ratings === 1 ? 'Poor' : ratings === 2 ? 'Fair' : ratings === 3 ? 'Good' : ratings === 4 ? 'Very Good' : 'Excellent'} {/* Review Text */} {/* Images */} {/* Submit */} {createReview.isPending ? 'Submitting...' : 'Submit Review'} ); }; export default ProductDetail;