From ed0c5a2a5f3a9c2af65c42b946eabe1862abbdd0 Mon Sep 17 00:00:00 2001 From: Isabella Pozzi <156220576+isabellakpozzi@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:09:24 -0500 Subject: [PATCH 1/9] photo picker component + integration into long post --- frontend/components/PhotoPicker.tsx | 194 ++++++++++++++++++++++++++++ frontend/components/PostForm.tsx | 35 ++++- 2 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 frontend/components/PhotoPicker.tsx diff --git a/frontend/components/PhotoPicker.tsx b/frontend/components/PhotoPicker.tsx new file mode 100644 index 00000000..faaa2401 --- /dev/null +++ b/frontend/components/PhotoPicker.tsx @@ -0,0 +1,194 @@ +import { useState } from 'react'; +import { View, TouchableOpacity, Text, Image, StyleSheet, Alert } from 'react-native'; +import * as ExpoImagePicker from 'expo-image-picker'; +import { MaterialIcons } from '@expo/vector-icons'; + +// testing shape for cropper - circle for now +interface ImagePickerProps { + onImageSelected?: (uri: string) => void; + currentImage?: string | null; + size?: 'small' | 'medium' | 'large'; + shape?: 'circle' | 'square'; +} + +export default function ImagePicker({ + onImageSelected, + currentImage, + size = 'medium', + shape = 'square' +}: ImagePickerProps) { + const [selectedImage, setSelectedImage] = useState(currentImage || null); + + const requestPermissions = async () => { + const { status } = await ExpoImagePicker.requestMediaLibraryPermissionsAsync(); + + if (status !== 'granted') { + Alert.alert( + 'Permission Required', + 'Enable permissions to upload a photo.', + [{ text: 'OK' }] + ); + return false; + } + return true; + }; + + const pickImage = async () => { + // might need to equest permissions + const hasPermission = await requestPermissions(); + if (!hasPermission) return; + + try { + const result = await ExpoImagePicker.launchImageLibraryAsync({ + mediaTypes: ExpoImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: shape === 'circle' ? [1, 1] : [4, 3], + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + const imageUri = result.assets[0].uri; + setSelectedImage(imageUri); + + if (onImageSelected) { + onImageSelected(imageUri); + } + } + } catch (error) { + console.error('Error picking image:', error); + Alert.alert('Error', 'Failed to pick image.'); + } + }; + + const removeImage = () => { + setSelectedImage(null); + if (onImageSelected) { + onImageSelected(''); + } + }; + + const sizeStyles = { + small: { width: 80, height: 80 }, + medium: { width: 120, height: 120 }, + large: { width: 200, height: 200 }, + }; + + const containerSize = sizeStyles[size]; + const isCircle = shape === 'circle'; + + return ( + + + {selectedImage ? ( + <> + + + + + + ) : ( + + + Add Photo + + )} + + + {selectedImage && ( + + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + }, + imageContainer: { + backgroundColor: '#f5f5f5', + borderRadius: 12, + overflow: 'hidden', + borderWidth: 2, + borderColor: '#e0e0e0', + borderStyle: 'dashed', + }, + circleContainer: { + borderRadius: 999, + }, + emptyContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + image: { + width: '100%', + height: '100%', + }, + circleImage: { + borderRadius: 999, + }, + overlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + justifyContent: 'center', + alignItems: 'center', + }, + placeholderContent: { + alignItems: 'center', + justifyContent: 'center', + }, + placeholderText: { + marginTop: 8, + fontSize: 14, + color: '#999', + fontWeight: '500', + }, + removeButton: { + position: 'absolute', + top: -8, + right: -8, + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#d32f2f', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, +}); \ No newline at end of file diff --git a/frontend/components/PostForm.tsx b/frontend/components/PostForm.tsx index ef11962a..eda8d6b9 100644 --- a/frontend/components/PostForm.tsx +++ b/frontend/components/PostForm.tsx @@ -1,25 +1,34 @@ import React, { useState } from 'react'; -import { View, TextInput, Button } from 'react-native'; +import { View, TextInput, Button, StyleSheet } from 'react-native'; +import ImagePicker from './PhotoPicker'; + interface PostFormProps { showTitle?: boolean; showStars?: boolean; showTextBox?: boolean; - onSubmit: (data: { title: string; content: string; rating: number }) => void; + showImagePicker?: boolean; + onSubmit: (data: { title: string; content: string; rating: number; imageUri?: string; }) => void; } export default function PostForm({ showTitle = false, showStars = false, showTextBox = true, + showImagePicker = true, onSubmit }: PostFormProps) { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [rating, setRating] = useState(0); + const [imageUri, setImageUri] = useState(null); const handleSubmit = () => { - onSubmit({ title, content, rating }); + onSubmit({ title, content, rating, imageUri: imageUri || undefined }); + }; + + const handleImageSelected = (uri: string) => { + setImageUri(uri || null); }; return ( @@ -73,8 +82,26 @@ export default function PostForm({ }} /> )} + + {showImagePicker && ( + + + + )}