import React, { useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert, ActivityIndicator, Platform, Image, KeyboardAvoidingView, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import * as ImagePicker from 'expo-image-picker'; import { uploadMedia } from '../../lib/mediaApi'; import { profileSettingsApi } from '../../lib/profileSettingsApi'; import { getDeviceInfo } from '../../lib/deviceInfo'; import { useAuthStore } from '../../store'; import { colors, gradients, spacing, radius, typography } from '../../theme'; import type { FeedbackType } from '../../types'; interface FeedbackModalProps { visible: boolean; onClose: () => void; } const FEEDBACK_OPTIONS: { type: FeedbackType; label: string; icon: keyof typeof Ionicons.glyphMap }[] = [ { type: 'bug', label: 'Bug', icon: 'bug' }, { type: 'feature_request', label: 'Feature', icon: 'bulb' }, { type: 'general', label: 'General', icon: 'chatbubble-ellipses' }, ]; export function FeedbackModal({ visible, onClose }: FeedbackModalProps) { const { user } = useAuthStore(); const [feedbackType, setFeedbackType] = useState('bug'); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [screenshotUri, setScreenshotUri] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [errors, setErrors] = useState<{ title?: string; description?: string }>({}); if (!visible) return null; const resetForm = () => { setFeedbackType('bug'); setTitle(''); setDescription(''); setScreenshotUri(null); setErrors({}); }; const handleClose = () => { if (!isSubmitting) { resetForm(); onClose(); } }; const handlePickScreenshot = async () => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: false, quality: 0.7, }); if (!result.canceled) { setScreenshotUri(result.assets[0].uri); } }; const uploadScreenshot = async (uri: string, _userId: string): Promise => { let ext = 'jpg'; if (Platform.OS === 'web') { const response = await fetch(uri); const blob = await response.blob(); if (blob.type === 'image/png') ext = 'png'; else if (blob.type === 'image/jpeg' || blob.type === 'image/jpg') ext = 'jpg'; else if (blob.type === 'image/gif') ext = 'gif'; else if (blob.type === 'image/webp') ext = 'webp'; } else { ext = uri.split('.').pop()?.toLowerCase() || 'jpg'; } return (await uploadMedia(uri, { kind: 'feedback', mimeType: `image/${ext === 'jpg' ? 'jpeg' : ext}` })).media.id; }; const handleSubmit = async () => { // Validate const newErrors: typeof errors = {}; if (!title.trim()) newErrors.title = 'Title is required'; if (!description.trim()) newErrors.description = 'Description is required'; if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } if (!user) { Alert.alert('Error', 'You must be logged in to submit feedback.'); return; } setIsSubmitting(true); try { const deviceInfo = getDeviceInfo(); let screenshotMediaId: string | null = null; // Upload screenshot if provided if (screenshotUri) { screenshotMediaId = await uploadScreenshot(screenshotUri, user.id); } await profileSettingsApi.submitFeedback({ type: feedbackType, title: title.trim(), description: description.trim(), device_info: deviceInfo, ...(screenshotMediaId ? { screenshot_media_id: screenshotMediaId } : {}), }); Alert.alert('Thank you!', 'Your feedback has been submitted successfully.', [ { text: 'OK', onPress: handleClose }, ]); resetForm(); } catch (error) { Alert.alert('Error', 'Failed to submit feedback. Please try again.'); console.error(error); } finally { setIsSubmitting(false); } }; const selectedOption = FEEDBACK_OPTIONS.find((o) => o.type === feedbackType)!; return ( {/* Close Button */} {/* Header */} Send Feedback Help us improve Sauci {/* Type Selector */} {FEEDBACK_OPTIONS.map((option) => ( setFeedbackType(option.type)} activeOpacity={0.7} > {option.label} ))} {/* Title Input */} Title { setTitle(text); if (errors.title) setErrors((e) => ({ ...e, title: undefined })); }} placeholder="Brief summary of your feedback" placeholderTextColor={colors.textTertiary} editable={!isSubmitting} /> {errors.title && {errors.title}} {/* Description Input */} Description { setDescription(text); if (errors.description) setErrors((e) => ({ ...e, description: undefined })); }} placeholder="Please provide details about the issue or suggestion..." placeholderTextColor={colors.textTertiary} multiline numberOfLines={4} textAlignVertical="top" editable={!isSubmitting} /> {errors.description && ( {errors.description} )} {/* Screenshot Picker */} Screenshot (optional) {screenshotUri ? ( setScreenshotUri(null)} disabled={isSubmitting} > ) : ( Tap to add a screenshot )} {/* Buttons */} Cancel {isSubmitting ? ( ) : ( <> Submit )} ); } const styles = StyleSheet.create({ fullScreen: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1000, }, backdrop: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.8)', }, keyboardView: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: spacing.lg, }, content: { backgroundColor: colors.backgroundLight, borderRadius: radius.xl, padding: spacing.lg, width: '100%', maxWidth: 400, maxHeight: '80%', borderWidth: 1, borderColor: colors.glass.border, }, closeButton: { position: 'absolute', top: spacing.md, right: spacing.md, zIndex: 1, padding: spacing.xs, }, scrollContent: { flexGrow: 1, }, header: { alignItems: 'center', marginBottom: spacing.lg, }, headerIcon: { width: 64, height: 64, borderRadius: 32, justifyContent: 'center', alignItems: 'center', marginBottom: spacing.md, }, headerTitle: { ...typography.title2, color: colors.text, textAlign: 'center', }, headerSubtitle: { ...typography.subhead, color: colors.textSecondary, textAlign: 'center', marginTop: spacing.xs, }, typeSelector: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.lg, }, typeButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: spacing.xs, backgroundColor: colors.glass.background, paddingVertical: spacing.sm, paddingHorizontal: spacing.md, borderRadius: radius.md, borderWidth: 1, borderColor: colors.glass.border, }, typeButtonActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, typeButtonText: { ...typography.caption1, color: colors.textSecondary, fontWeight: '600', }, typeButtonTextActive: { color: colors.text, }, inputGroup: { marginBottom: spacing.md, }, label: { ...typography.subhead, color: colors.textSecondary, marginBottom: spacing.sm, marginLeft: spacing.xs, }, input: { backgroundColor: colors.glass.background, borderRadius: radius.md, borderWidth: 1, borderColor: colors.glass.border, padding: spacing.md, ...typography.body, color: colors.text, }, inputError: { borderColor: colors.error, }, textArea: { minHeight: 100, paddingTop: spacing.md, }, errorText: { ...typography.caption1, color: colors.error, marginTop: spacing.xs, marginLeft: spacing.xs, }, screenshotPicker: { backgroundColor: colors.glass.background, borderRadius: radius.md, borderWidth: 1, borderColor: colors.glass.border, borderStyle: 'dashed', padding: spacing.lg, alignItems: 'center', justifyContent: 'center', gap: spacing.sm, }, screenshotPickerText: { ...typography.subhead, color: colors.textTertiary, }, screenshotPreview: { position: 'relative', alignSelf: 'flex-start', }, screenshotImage: { width: 120, height: 120, borderRadius: radius.md, backgroundColor: colors.glass.background, }, removeScreenshot: { position: 'absolute', top: -8, right: -8, backgroundColor: colors.backgroundLight, borderRadius: 12, }, buttons: { flexDirection: 'row', gap: spacing.md, marginTop: spacing.lg, }, cancelButton: { flex: 1, backgroundColor: colors.glass.backgroundLight, paddingVertical: spacing.md, borderRadius: radius.md, alignItems: 'center', borderWidth: 1, borderColor: colors.glass.borderLight, }, cancelButtonText: { ...typography.body, color: colors.textSecondary, fontWeight: '600', }, submitButton: { flex: 1, backgroundColor: colors.primary, paddingVertical: spacing.md, borderRadius: radius.md, alignItems: 'center', flexDirection: 'row', justifyContent: 'center', gap: spacing.xs, }, submitButtonText: { ...typography.body, color: colors.text, fontWeight: '600', }, }); export default FeedbackModal;