eletrotupi / tcc/ frontend/components/ui/AvatarUpload.tsx19a4d910605b887720e8df1e86ba782bbbaba23f
7.7 KB | 303 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 { File } from 'expo-file-system';
14
import { Ionicons } from '@expo/vector-icons';
15
import { apiClient, ApiError } from '@/lib/api';
16
import { useToast } from '@/context/ToastContext';
17
import { translateError } from '@/lib/errors/translations';
18
import { Colors } from '@/constants/theme';
19
import type { User } from '@/lib/api/types';
20

21
interface AvatarUploadProps {
22
  currentAvatar?: string | null;
23
  onUploadSuccess?: (user: User) => void;
24
  onRemoveSuccess?: (user: User) => void;
25
}
26

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

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

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

62
  const uploadAvatar = async (imageUri: string): Promise<void> => {
63
    setIsLoading(true);
64

65
    try {
66
      const formData = new FormData();
67
      formData.append('avatar', new File(imageUri));
68

69
      const { user } = await apiClient.avatar(formData);
70

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

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

96
          try {
97
            const { user } = await apiClient.removeAvatar();
98

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

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

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

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

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

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

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

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

303
export default AvatarUpload;