289 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect, useCallback } from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
Animated,
ScrollView,
Image,
KeyboardAvoidingView,
Platform,
TouchableWithoutFeedback,
} from 'react-native';
import { useAppTheme } from '@theme';
import { RatingStars } from '../ratingStars/ratingStars';
import { getStyles } from './productRatingModal.styles';
import { giveRatingPayload } from '@interfaces';
// ─── Types ────────────────────────────────────────────────────────────────────
export interface ProductRatingModalProps {
visible: boolean;
/** Product id being rated */
productId: string;
/** Display name of the product */
productName: string;
/** Optional: qty badge text, e.g. "2x" */
quantity?: string | number;
/** The parent order id — required by the rating API */
orderId?: string;
/** Called when the user submits their rating */
onSubmit: (payload: giveRatingPayload) => void;
/** Called when the modal is dismissed without submitting */
onClose: () => void;
}
// ─── Rating hint labels ───────────────────────────────────────────────────────
const RATING_LABELS: Record<number, string> = {
1: 'Terrible 😞',
2: 'Bad 😕',
3: 'Okay 😐',
4: 'Good 😊',
5: 'Excellent 🤩',
};
const MAX_COMMENT = 300;
// ─── Component ────────────────────────────────────────────────────────────────
export const ProductRatingModal: React.FC<ProductRatingModalProps> = ({
visible,
orderId,
productId,
productName,
quantity,
onSubmit,
onClose,
}) => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
// ── State ────────────────────────────────────────────────────────────────
const [rating, setRating] = useState(0);
const [comment, setComment] = useState('');
const [commentFocused, setCommentFocused] = useState(false);
const [imageUris] = useState<string[]>([]); // placeholder wire launchImageLibrary here
const [submitted, setSubmitted] = useState(false);
// ── Animation ────────────────────────────────────────────────────────────
const slideY = useRef(new Animated.Value(400)).current;
useEffect(() => {
if (visible) {
// reset state each time the modal opens for a new product
setRating(0);
setComment('');
setSubmitted(false);
Animated.spring(slideY, {
toValue: 0,
useNativeDriver: true,
damping: 18,
stiffness: 160,
}).start();
} else {
slideY.setValue(400);
}
}, [visible, slideY]);
// ── Handlers ─────────────────────────────────────────────────────────────
const handleClose = useCallback(() => {
Animated.timing(slideY, {
toValue: 400,
duration: 200,
useNativeDriver: true,
}).start(onClose);
}, [slideY, onClose]);
const handleSubmit = useCallback(() => {
if (rating === 0) return;
onSubmit({
rating,
comment,
images: imageUris,
orderId: orderId ?? '',
});
setSubmitted(true);
}, [rating, comment, imageUris, orderId, onSubmit]);
const canSubmit = rating > 0;
// ── Render ───────────────────────────────────────────────────────────────
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={handleClose}
statusBarTranslucent
>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
{/* Backdrop — tap to dismiss */}
<TouchableWithoutFeedback onPress={handleClose}>
<View style={styles.overlay}>
<TouchableWithoutFeedback>
<Animated.View
style={[styles.sheet, { transform: [{ translateY: slideY }] }]}
>
{/* Drag handle */}
<View style={styles.dragHandle} />
{/* Product hero */}
<View style={styles.productHero}>
<Text style={styles.productEmoji}>🛍</Text>
<Text style={styles.productName} numberOfLines={2}>
{productName}
</Text>
{quantity !== undefined && (
<Text style={styles.productMeta}>Qty: {quantity}</Text>
)}
</View>
{/* Already-submitted banner */}
{submitted ? (
<>
<View style={styles.ratedBanner}>
<Text style={styles.ratedBannerEmoji}></Text>
<Text style={styles.ratedBannerText}>
Thanks for your rating!
</Text>
</View>
{/* Show submitted stars (read-only) */}
<View style={styles.ratingSection}>
<RatingStars rating={rating} size={34} />
</View>
<TouchableOpacity
style={[styles.submitBtn, styles.submitBtnActive]}
onPress={handleClose}
activeOpacity={0.8}
>
<Text style={styles.submitBtnText}>Done</Text>
</TouchableOpacity>
</>
) : (
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Star rating */}
<View style={styles.ratingSection}>
<Text style={styles.ratingLabel}>Your Rating</Text>
<RatingStars
rating={rating}
onRate={setRating}
size={40}
/>
<Text style={styles.ratingHint}>
{rating > 0
? RATING_LABELS[rating]
: 'Tap a star to rate'}
</Text>
</View>
<View style={styles.divider} />
{/* Comment (optional) */}
<View style={styles.inputSection}>
<Text style={styles.inputLabel}>
Review{' '}
<Text style={styles.optionalTag}>(optional)</Text>
</Text>
<TextInput
style={[
styles.commentInput,
commentFocused && styles.commentInputFocused,
]}
placeholder="Share your experience with this product..."
placeholderTextColor={colors.textSecondary}
value={comment}
onChangeText={t => setComment(t.slice(0, MAX_COMMENT))}
multiline
onFocus={() => setCommentFocused(true)}
onBlur={() => setCommentFocused(false)}
returnKeyType="done"
blurOnSubmit
/>
<Text style={styles.charCount}>
{comment.length}/{MAX_COMMENT}
</Text>
</View>
{/* Image upload strip (optional) */}
<View style={styles.imageSection}>
<Text style={styles.imageLabel}>
Photos{' '}
<Text style={styles.optionalTag}>(optional)</Text>
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.imageScrollContent}
>
{/* Add photo button */}
<TouchableOpacity
style={styles.addImageBtn}
activeOpacity={0.75}
/* TODO: wire react-native-image-picker here */
>
<Text style={styles.addImageIcon}>📷</Text>
<Text style={styles.addImageText}>Add</Text>
</TouchableOpacity>
{/* Thumbnail previews */}
{imageUris.map((uri, i) => (
<View key={i} style={styles.imageThumb}>
<Image
source={{ uri }}
style={styles.imageThumbnail}
/>
<TouchableOpacity
style={styles.removeImageBtn}
hitSlop={{ top: 4, right: 4, bottom: 4, left: 4 }}
>
<Text style={styles.removeImageText}></Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
</View>
{/* Submit */}
<TouchableOpacity
style={[
styles.submitBtn,
canSubmit
? styles.submitBtnActive
: styles.submitBtnDisabled,
]}
onPress={handleSubmit}
disabled={!canSubmit}
activeOpacity={0.85}
>
<Text
style={[
styles.submitBtnText,
!canSubmit && styles.submitBtnTextDisabled,
]}
>
Submit Review
</Text>
</TouchableOpacity>
</ScrollView>
)}
</Animated.View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
</Modal>
);
};