eletrotupi / tcc/ frontend/components/ui/AvatarUpload.tsxab63193481c4d9f2e6f4506310c89df74af00d38
7.4 KB | 296 lines Raw
1
import React, { useState, useRef } from 'react';
2
import {
3
  View,
4
  Image,
5
  TouchableOpacity,
6
  Text,
7
  Alert,
8
  ActivityIndicator,
9
  Animated,
10
  StyleSheet,
11
} from 'react-native';
12
import * as ImagePicker from 'expo-image-picker';
13
import { Ionicons } from '@expo/vector-icons';
14
import { apiClient } from '@/lib/api';
15
import { Colors } from '@/constants/theme';
16

17
interface AvatarUploadProps {
18
  currentAvatar?: string | null;
19
  onUploadSuccess?: () => void;
20
  onRemoveSuccess?: () => void;
21
}
22

23
const AvatarUpload: React.FC<AvatarUploadProps> = ({
24
  currentAvatar = null,
25
  onUploadSuccess,
26
  onRemoveSuccess,
27
}) => {
28
  const [preview, setPreview] = useState<string | null>(currentAvatar);
29
  const [isLoading, setIsLoading] = useState<boolean>(false);
30
  const scaleAnim = useRef(new Animated.Value(1)).current;
31

32
  const handleImagePick = async (useCamera: boolean = false): Promise<void> => {
33
    try {
34
      const result = useCamera
35
        ? await ImagePicker.launchCameraAsync({
36
            allowsEditing: true,
37
            aspect: [1, 1],
38
            quality: 0.8,
39
          })
40
        : await ImagePicker.launchImageLibraryAsync({
41
            allowsEditing: true,
42
            aspect: [1, 1],
43
            quality: 0.8,
44
          });
45

46
      if (!result.canceled && result.assets && result.assets.length > 0) {
47
        const asset = result.assets[0];
48
        setPreview(asset.uri);
49
        await uploadAvatar(asset.fileName, asset.uri, asset.mimeType);
50
      }
51
    } catch (error) {
52
      console.error('Image picker error:', error);
53
      Alert.alert('Error', 'Failed to select image');
54
    }
55
  };
56

57
  const uploadAvatar = async (fileName: string, imageUri: string, mimeType: string): Promise<void> => {
58
    setIsLoading(true);
59

60
    try {
61
      const formData = new FormData();
62
      formData.append('avatar', {
63
        uri: imageUri,
64
        type: mimeType,
65
        name: fileName,
66
      } as any);
67

68
      const response = await apiClient.avatar(formData);
69

70
      onUploadSuccess?.(response.user);
71
    } catch (error) {
72
      console.error('Upload error:', error);
73
      Alert.alert('Error', error instanceof Error ? error.message : 'Upload failed');
74
      setPreview(currentAvatar);
75
    } finally {
76
      setIsLoading(false);
77
    }
78
  };
79

80
  const handleRemove = (): void => {
81
    Alert.alert('Remover sua foto', 'Tem certeza que quer remover sua foto de perfil?', [
82
      {
83
        text: 'Cancelar',
84
        onPress: () => {},
85
        style: 'cancel',
86
      },
87
      {
88
        text: 'Remover',
89
        onPress: async () => {
90
          setIsLoading(true);
91

92
          try {
93
            const response = await apiClient.removeAvatar();
94

95
            setPreview(null);
96
            onRemoveSuccess?.(response.user);
97
          } catch (error) {
98
            console.error('Remove error:', error);
99
            Alert.alert('Error', error instanceof Error ? error.message : 'Failed to remove avatar');
100
          } finally {
101
            setIsLoading(false);
102
          }
103
        },
104
        style: 'destructive',
105
      },
106
    ]);
107
  };
108

109
  const handlePressIn = (): void => {
110
    Animated.spring(scaleAnim, {
111
      toValue: 0.95,
112
      useNativeDriver: true,
113
    }).start();
114
  };
115

116
  const handlePressOut = (): void => {
117
    Animated.spring(scaleAnim, {
118
      toValue: 1,
119
      useNativeDriver: true,
120
    }).start();
121
  };
122

123
  const showImageOptions = (): void => {
124
    Alert.alert('Selecionar uma foto de perfil', 'Escolha de onde você quer:', [
125
      {
126
        text: 'Câmera',
127
        onPress: () => handleImagePick(true),
128
      },
129
      {
130
        text: 'Galeria',
131
        onPress: () => handleImagePick(false),
132
      },
133
      {
134
        text: 'Cancelar',
135
        style: 'cancel',
136
      },
137
    ]);
138
  };
139

140
  return (
141
    <View style={styles.container}>
142
      <View style={styles.avatarSection}>
143
        {preview ? (
144
          <View style={styles.avatarWrapper}>
145
            <Image source={{ uri: preview }} style={styles.avatarImage} />
146
            {isLoading && (
147
              <View style={styles.loadingOverlay}>
148
                <ActivityIndicator color="#fff" size="large" />
149
              </View>
150
            )}
151
          </View>
152
        ) : (
153
          <TouchableOpacity
154
            style={styles.avatarPlaceholder}
155
            onPress={showImageOptions}
156
            activeOpacity={0.7}
157
            disabled={isLoading}
158
          >
159
            <Ionicons name="cloud-upload-outline" size={32} color="#666" />
160
            <Text style={styles.placeholderText}>Adicione uma foto</Text>
161
          </TouchableOpacity>
162
        )}
163
      </View>
164

165
      <View style={styles.avatarActions}>
166
        <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
167
          <TouchableOpacity
168
            style={[styles.actionBtn, styles.editBtn]}
169
            onPress={showImageOptions}
170
            onPressIn={handlePressIn}
171
            onPressOut={handlePressOut}
172
            disabled={isLoading}
173
            activeOpacity={0.8}
174
          >
175
            {isLoading ? (
176
              <ActivityIndicator color="#fff" size="small" />
177
            ) : (
178
              <Text style={styles.editBtnText}>ALTERAR FOTO</Text>
179
            )}
180
          </TouchableOpacity>
181
        </Animated.View>
182

183
        {preview && (
184
          <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
185
            <TouchableOpacity
186
              style={[styles.actionBtn, styles.removeBtn]}
187
              onPress={handleRemove}
188
              onPressIn={handlePressIn}
189
              onPressOut={handlePressOut}
190
              disabled={isLoading}
191
              activeOpacity={0.8}
192
            >
193
              <Text style={styles.removeBtnText}>REMOVER FOTO</Text>
194
            </TouchableOpacity>
195
          </Animated.View>
196
        )}
197
      </View>
198
    </View>
199
  );
200
};
201

202
const styles = StyleSheet.create({
203
  container: {
204
    alignItems: 'center',
205
    gap: 32,
206
    paddingVertical: 32,
207
  },
208
  avatarSection: {
209
    position: 'relative',
210
    width: 160,
211
    height: 160,
212
  },
213
  avatarWrapper: {
214
    width: '100%',
215
    height: '100%',
216
    borderRadius: 80,
217
    overflow: 'hidden',
218
    backgroundColor: '#f0f0f0',
219
    shadowColor: '#000',
220
    shadowOffset: { width: 0, height: 8 },
221
    shadowOpacity: 0.15,
222
    shadowRadius: 12,
223
    elevation: 8,
224
  },
225
  avatarImage: {
226
    width: '100%',
227
    height: '100%',
228
    resizeMode: 'cover',
229
  },
230
  loadingOverlay: {
231
    position: 'absolute',
232
    top: 0,
233
    left: 0,
234
    right: 0,
235
    bottom: 0,
236
    backgroundColor: 'rgba(0, 0, 0, 0.4)',
237
    alignItems: 'center',
238
    justifyContent: 'center',
239
  },
240
  avatarPlaceholder: {
241
    width: '100%',
242
    height: '100%',
243
    borderRadius: 80,
244
    borderWidth: 2,
245
    borderStyle: 'dashed',
246
    borderColor: '#d0d0d0',
247
    display: 'flex',
248
    alignItems: 'center',
249
    justifyContent: 'center',
250
    backgroundColor: '#fafafa',
251
  },
252
  placeholderText: {
253
    marginTop: 8,
254
    fontSize: 12,
255
    fontWeight: '500',
256
    color: '#666',
257
    textAlign: 'center',
258
  },
259
  avatarActions: {
260
    display: 'flex',
261
    flexDirection: 'row',
262
    gap: 16,
263
    width: '100%',
264
    maxWidth: 320,
265
    justifyContent: 'center',
266
    flexWrap: 'wrap',
267
  },
268
  actionBtn: {
269
    paddingVertical: 12,
270
    paddingHorizontal: 24,
271
    borderRadius: 6,
272
    justifyContent: 'center',
273
    alignItems: 'center',
274
    minWidth: 140,
275
  },
276
  editBtn: {
277
  },
278
  editBtnText: {
279
    fontSize: 12,
280
    fontWeight: '600',
281
    color: Colors.light.textSecondary,
282
    letterSpacing: 0.5,
283
    textTransform: 'uppercase',
284
  },
285
  removeBtn: {
286
  },
287
  removeBtnText: {
288
    fontSize: 12,
289
    fontWeight: '600',
290
    color: Colors.light.danger,
291
    letterSpacing: 0.5,
292
    textTransform: 'uppercase',
293
  },
294
});
295

296
export default AvatarUpload;