eletrotupi / tcc/ commit / 7315aa4

frontend: introduce avatar upload component

Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago 7315aa48399c26dd3cc6e78613bbd578a8e8d2b7 Browse files
Parents a5fffad
Changed files
1
Additions
+296
Deletions
0
/dev/null → frontend/components/ui/AvatarUpload.tsx
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;