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

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

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

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

49
      if (!result.canceled && result.assets && result.assets.length > 0) {
50
        const asset = result.assets[0];
51
        setPreview(asset.uri);
52
        await uploadAvatar(asset.fileName, asset.uri, asset.mimeType);
53
      }
54
    } catch (error) {
55
      console.error('Image picker error:', error);
56
      showToast('Falha ao selecionar imagem', 'error');
57
    }
58
  };
59

60
  const uploadAvatar = async (fileName: string, imageUri: string, mimeType: string): Promise<void> => {
61
    setIsLoading(true);
62

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

71
      const response = await apiClient.avatar(formData);
72

73
      onUploadSuccess?.(response.user);
74
    } catch (error) {
75
      console.error('Upload error:', error);
76
      showToast(
77
        error instanceof ApiError ? translateError(error.message) : 'Falha ao enviar foto',
78
        'error',
79
      );
80
      setPreview(currentAvatar);
81
    } finally {
82
      setIsLoading(false);
83
    }
84
  };
85

86
  const handleRemove = (): void => {
87
    Alert.alert('Remover sua foto', 'Tem certeza que quer remover sua foto de perfil?', [
88
      {
89
        text: 'Cancelar',
90
        onPress: () => {},
91
        style: 'cancel',
92
      },
93
      {
94
        text: 'Remover',
95
        onPress: async () => {
96
          setIsLoading(true);
97

98
          try {
99
            const response = await apiClient.removeAvatar();
100

101
            setPreview(null);
102
            onRemoveSuccess?.(response.user);
103
          } catch (error) {
104
            console.error('Remove error:', error);
105
            showToast(
106
              error instanceof ApiError ? translateError(error.message) : 'Falha ao remover foto',
107
              'error',
108
            );
109
          } finally {
110
            setIsLoading(false);
111
          }
112
        },
113
        style: 'destructive',
114
      },
115
    ]);
116
  };
117

118
  const handlePressIn = (): void => {
119
    Animated.spring(scaleAnim, {
120
      toValue: 0.95,
121
      useNativeDriver: true,
122
    }).start();
123
  };
124

125
  const handlePressOut = (): void => {
126
    Animated.spring(scaleAnim, {
127
      toValue: 1,
128
      useNativeDriver: true,
129
    }).start();
130
  };
131

132
  const showImageOptions = (): void => {
133
    Alert.alert('Selecionar uma foto de perfil', 'Escolha de onde você quer:', [
134
      {
135
        text: 'Câmera',
136
        onPress: () => handleImagePick(true),
137
      },
138
      {
139
        text: 'Galeria',
140
        onPress: () => handleImagePick(false),
141
      },
142
      {
143
        text: 'Cancelar',
144
        style: 'cancel',
145
      },
146
    ]);
147
  };
148

149
  return (
150
    <View style={styles.container}>
151
      <View style={styles.avatarSection}>
152
        {preview ? (
153
          <View style={styles.avatarWrapper}>
154
            <Image source={{ uri: preview }} style={styles.avatarImage} />
155
            {isLoading && (
156
              <View style={styles.loadingOverlay}>
157
                <ActivityIndicator color="#fff" size="large" />
158
              </View>
159
            )}
160
          </View>
161
        ) : (
162
          <TouchableOpacity
163
            style={styles.avatarPlaceholder}
164
            onPress={showImageOptions}
165
            activeOpacity={0.7}
166
            disabled={isLoading}
167
          >
168
            <Ionicons name="cloud-upload-outline" size={32} color="#666" />
169
            <Text style={styles.placeholderText}>Adicione uma foto</Text>
170
          </TouchableOpacity>
171
        )}
172
      </View>
173

174
      <View style={styles.avatarActions}>
175
        <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
176
          <TouchableOpacity
177
            style={[styles.actionBtn, styles.editBtn]}
178
            onPress={showImageOptions}
179
            onPressIn={handlePressIn}
180
            onPressOut={handlePressOut}
181
            disabled={isLoading}
182
            activeOpacity={0.8}
183
          >
184
            {isLoading ? (
185
              <ActivityIndicator color="#fff" size="small" />
186
            ) : (
187
              <Text style={styles.editBtnText}>ALTERAR FOTO</Text>
188
            )}
189
          </TouchableOpacity>
190
        </Animated.View>
191

192
        {preview && (
193
          <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
194
            <TouchableOpacity
195
              style={[styles.actionBtn, styles.removeBtn]}
196
              onPress={handleRemove}
197
              onPressIn={handlePressIn}
198
              onPressOut={handlePressOut}
199
              disabled={isLoading}
200
              activeOpacity={0.8}
201
            >
202
              <Text style={styles.removeBtnText}>REMOVER FOTO</Text>
203
            </TouchableOpacity>
204
          </Animated.View>
205
        )}
206
      </View>
207
    </View>
208
  );
209
};
210

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

305
export default AvatarUpload;