import React, { useState, useEffect } from 'react'; import { View, Text, ScrollView, TouchableOpacity, Alert, TextInput, Dimensions, ActivityIndicator, Platform } from 'react-native'; import { Image } from 'expo-image'; import { useRouter, useLocalSearchParams, Stack } from 'expo-router'; import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploader, ImageCarousel } from 'common-ui'; import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons'; import { trpc } from '@/src/trpc-client'; import usePickImage from 'common-ui/src/components/use-pick-image'; import { Formik } from 'formik'; import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; import Animated, { FadeInDown, FadeInUp, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; const { width: screenWidth } = Dimensions.get("window"); const carouselHeight = screenWidth * 0.85; interface ReviewResponseFormProps { reviewId: number; onClose: () => void; } const ReviewResponseForm: React.FC = ({ reviewId, onClose }) => { const [adminResponse, setAdminResponse] = useState(''); const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]); const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]); const [uploadUrls, setUploadUrls] = useState([]); const respondToReview = trpc.admin.product.respondToReview.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 (adminResponse: string) => { try { const mimeTypes = selectedImages.map(s => s.mimeType); const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({ contextString: 'review', mimeTypes, }); const keys = generatedUrls.map(url => { const u = new URL(url); const rawKey = u.pathname.replace(/^\/+/, ""); const decodedKey = decodeURIComponent(rawKey); const parts = decodedKey.split('/'); parts.shift(); return parts.join('/'); }); setUploadUrls(generatedUrls); for (let i = 0; i < generatedUrls.length; i++) { const uploadUrl = generatedUrls[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}`); } await respondToReview.mutateAsync({ reviewId, adminResponse, adminResponseImages: keys, uploadUrls: generatedUrls, }); Alert.alert('Success', 'Response submitted'); onClose(); setAdminResponse(''); setSelectedImages([]); setDisplayImages([]); setUploadUrls([]); } catch (error:any) { Alert.alert('Error', error.message || 'Failed to submit response.'); } }; return ( handleSubmit(values.adminResponse)} > {({ handleChange, handleSubmit: formikSubmit, values }) => ( Attach Images formikSubmit()} activeOpacity={0.8} disabled={respondToReview.isPending} > {respondToReview.isPending ? ( ) : ( Submit Response )} )} ); }; export default function ProductDetail() { const { id } = useLocalSearchParams(); const router = useRouter(); const productId = parseInt(id as string); const { data: productData, isLoading, error, refetch } = trpc.admin.product.getProductById.useQuery({ id: productId }); const { data: reviewsData } = trpc.admin.product.getProductReviews.useQuery({ productId }); const [responseDialogOpen, setResponseDialogOpen] = useState(false); const [selectedReview, setSelectedReview] = useState(null); useMarkDataFetchers(() => { refetch(); }); const toggleOutOfStock = trpc.admin.product.toggleOutOfStock.useMutation(); const product = productData?.product; const handleEdit = () => { router.push(`/edit-product?id=${productId}` as any); }; if (isLoading) { return ( Loading... ); } if (error || !product) { return ( Product Not Found router.back()} style={tw`mt-6 px-8 py-3 bg-gray-100 rounded-full`}> Go Back ); } return ( {/* Hero Section */} {product.images && product.images.length > 0 ? ( ) : ( )} {/* Gradient Overlay */} {/* Floating Header Buttons */} router.back()} activeOpacity={0.8}> Edit {/* Content Container - Overlapping the image slightly */} {/* Main Info Card */} {product.name} { toggleOutOfStock.mutate({ id: productId }, { onSuccess: () => Alert.alert('Success', 'Stock status updated'), onError: (err) => Alert.alert('Error', err.message) }); }} activeOpacity={0.9} > {product.isOutOfStock ? 'Out of Stock' : 'In Stock'} ₹{product.price} / {product.unit?.shortNotation} {product.marketPrice && ( ₹{product.marketPrice} )} {/* Increment Step Info */} Increment: {product.incrementStep || 1} {/* Quick Stats Row */} {reviewsData?.reviews.reduce((acc, r) => acc + r.ratings, 0) ? (reviewsData.reviews.reduce((acc, r) => acc + r.ratings, 0) / reviewsData.reviews.length).toFixed(1) : '-'} Rating {reviewsData?.reviews.length || 0} Reviews {/* { toggleOutOfStock.mutate({ id: productId }, { onSuccess: () => Alert.alert('Success', 'Stock status updated'), onError: (err) => Alert.alert('Error', err.message) }); }} activeOpacity={0.9} > {product.isOutOfStock ? 'Out of Stock' : 'In Stock'} Stock */} {/* Description */} Description {product.shortDescription && ( {product.shortDescription} )} {product.longDescription || "No detailed description available for this product."} {/* Availability */} Availability This product is currently {product.isOutOfStock ? 'out of stock' : 'in stock'}. { toggleOutOfStock.mutate({ id: productId }, { onSuccess: () => { Alert.alert('Success', 'Stock status updated'); refetch(); }, onError: (err) => Alert.alert('Error', err.message) }); }} activeOpacity={0.8} style={tw`bg-gray-100 px-4 py-2 rounded-full border border-gray-200 self-start`} > Mark as {product.isOutOfStock ? 'In Stock' : 'Out of Stock'} {/* Special Deals */} {product.deals && product.deals.length > 0 && ( Special Deals {product.deals.map((deal, index) => ( Buy {deal.quantity} Valid until {new Date(deal.validTill).toLocaleDateString()} ₹{deal.price} Total Price ))} )} {/* Reviews Section */} Reviews {reviewsData?.reviews.length || 0} Total {reviewsData && reviewsData.reviews.length > 0 ? ( reviewsData.reviews.map((review, idx) => ( {review.userName} {[1, 2, 3, 4, 5].map((star) => ( ))} {review.reviewBody} {review.signedImageUrls && review.signedImageUrls.length > 0 && ( {review.signedImageUrls.map((url, index) => ( ))} )} {/* Admin Response Section */} {review.adminResponse ? ( Admin Response {review.adminResponse} {review.signedAdminImageUrls && review.signedAdminImageUrls.length > 0 && ( {review.signedAdminImageUrls.map((url, index) => ( ))} )} ) : ( { setSelectedReview(review); setResponseDialogOpen(true); }} style={tw`mt-2 self-end px-4 py-2 bg-white rounded-full border border-gray-200 shadow-sm flex-row items-center`} > Reply )} )) ) : ( No reviews yet )} {/* Response Dialog */} setResponseDialogOpen(false)}> Reply to Review setResponseDialogOpen(false)} style={tw`p-2 bg-gray-100 rounded-full`}> {selectedReview && ( Replying to {selectedReview.userName} {selectedReview.reviewBody} { setResponseDialogOpen(false); setSelectedReview(null); }} /> )} ); }