diff --git a/backend/src/controllers/comment.ts b/backend/src/controllers/comment.ts index 142a686..b813816 100644 --- a/backend/src/controllers/comment.ts +++ b/backend/src/controllers/comment.ts @@ -1,6 +1,7 @@ import { Response, Request } from "express"; import type { AuthenticatedRequest } from "../middleware/auth"; import { prisma } from "../services/db"; +import { randomUUID } from "crypto"; /** * GET /api/comment/:id @@ -141,6 +142,7 @@ export const createComment = async (req: AuthenticatedRequest, res: Response) => try { const newComment = await prisma.comment.create({ data: { + id: randomUUID(), userId: req.user.id, postId: postId ?? null, parentId: parentId ?? null, @@ -349,6 +351,7 @@ export const toggleCommentLike = async (req: AuthenticatedRequest, res: Response // Like - add a new like await prisma.commentLike.create({ data: { + id: randomUUID(), commentId, userId: req.user.id, }, diff --git a/backend/src/controllers/post.ts b/backend/src/controllers/post.ts index 6c4ed2b..637389e 100644 --- a/backend/src/controllers/post.ts +++ b/backend/src/controllers/post.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { prisma } from "../services/db.js"; import { Prisma } from "@prisma/client"; +import { randomUUID } from "crypto"; // CREATE POST export const createPost = async (req: Request, res: Response) => { @@ -53,18 +54,19 @@ export const createPost = async (req: Request, res: Response) => { } } - const newPost = await prisma.post.create({ - data: { - userId, - movieId, - content, - type: type || "SHORT", - stars: stars ? parseInt(stars, 10) : null, - spoiler: spoiler || false, - tags: tags || [], - imageUrls: imageUrls || [], - repostedPostId, - }, + const newPost = await prisma.post.create({ + data: { + id: randomUUID(), + userId, + movieId, + content, + type: type || "SHORT", + stars: stars ? parseInt(stars, 10) : null, + spoiler: spoiler || false, + tags: tags || [], + imageUrls: imageUrls || [], + repostedPostId, + }, include: { UserProfile: { select: { @@ -99,6 +101,7 @@ export const createPost = async (req: Request, res: Response) => { export const getPostById = async (req: Request, res: Response) => { try { const { postId } = req.params; + const { currentUserId } = req.query; if (!postId) { return res.status(400).json({ message: "Post ID is required" }); @@ -133,7 +136,12 @@ export const getPostById = async (req: Request, res: Response) => { createdAt: "desc", }, }, - PostReaction: true, + PostReaction: { + select: { + reactionType: true, + userId: true, + }, + }, other_Post: { include: { UserProfile: { @@ -153,6 +161,19 @@ export const getPostById = async (req: Request, res: Response) => { if (!post) { return res.status(404).json({ message: "Post not found" }); } + + // Count reactions by type + const reactionCounts = post.PostReaction.reduce((acc: Record, r: any) => { + acc[r.reactionType] = (acc[r.reactionType] || 0) + 1; + return acc; + }, {}); + + // Get current user's reactions if provided + const userReactions = currentUserId + ? post.PostReaction + .filter(r => r.userId === currentUserId) + .map(r => r.reactionType) + : []; res.json({ message: "Post found successfully", @@ -160,6 +181,8 @@ export const getPostById = async (req: Request, res: Response) => { ...post, Reposts: post.other_Post, reactionCount: post.PostReaction.length, + reactionCounts, + userReactions, commentCount: post.Comment.length, repostCount: post.other_Post.length, }, @@ -438,6 +461,7 @@ export const toggleReaction = async (req: Request, res: Response) => { // Add reaction await prisma.postReaction.create({ data: { + id: randomUUID(), postId, userId, reactionType, diff --git a/backend/src/controllers/ratings.ts b/backend/src/controllers/ratings.ts index cfd1de1..c627123 100644 --- a/backend/src/controllers/ratings.ts +++ b/backend/src/controllers/ratings.ts @@ -1,6 +1,7 @@ import type { Response,Request } from 'express'; import type { AuthenticatedRequest } from '../middleware/auth'; import { prisma } from '../services/db'; +import { randomUUID } from 'crypto'; export const createRating = async (req: AuthenticatedRequest, res: Response) => { const timestamp = new Date().toISOString(); @@ -13,6 +14,7 @@ export const createRating = async (req: AuthenticatedRequest, res: Response) => return res.status(400).json({ message: 'Stars must be between 0 and 5', timestamp, endpoint: '/api/ratings' }); } const newRatingData = { + id: randomUUID(), userId: req.user.id, movieId: req.body.movieId, stars, diff --git a/backend/src/tests/unit/post-reactions.unit.test.ts b/backend/src/tests/unit/post-reactions.unit.test.ts index a7adffb..30cd8d7 100644 --- a/backend/src/tests/unit/post-reactions.unit.test.ts +++ b/backend/src/tests/unit/post-reactions.unit.test.ts @@ -107,11 +107,11 @@ describe("Post Reactions Controller Unit Tests", () => { await toggleReaction(mockRequest as Request, mockResponse as Response); expect(prisma.postReaction.create).toHaveBeenCalledWith({ - data: { + data: expect.objectContaining({ postId: mockPostId, userId: mockUserId, reactionType, - }, + }), }); expect(responseObject.json).toHaveBeenCalledWith({ message: "Reaction added successfully", diff --git a/backend/src/types/models.ts b/backend/src/types/models.ts index 37c188f..555e1d8 100644 --- a/backend/src/types/models.ts +++ b/backend/src/types/models.ts @@ -197,6 +197,7 @@ export type GetMovieSummaryEnvelope = { }; export type PostFormData = { + userId: string; movieId: string; content: string; type: 'SHORT' | 'LONG'; diff --git a/frontend/app/profilePage/components/PostsList.tsx b/frontend/app/profilePage/components/PostsList.tsx index c5d52f5..3678842 100644 --- a/frontend/app/profilePage/components/PostsList.tsx +++ b/frontend/app/profilePage/components/PostsList.tsx @@ -6,6 +6,8 @@ import { ActivityIndicator, TouchableOpacity, FlatList, + StyleSheet, + Dimensions, } from 'react-native'; import tw from 'twrnc'; import { User } from '../../../lib/profilePage/_types'; @@ -13,6 +15,16 @@ import { Feather } from '@expo/vector-icons'; import { getPosts } from '../../../services/postsService'; import type { components } from '../../../types/api-generated'; import { formatCount } from '../../../lib/profilePage/_utils'; +import ReviewPost from '../../../components/ReviewPost'; +import PicturePost from '../../../components/PicturePost'; +import TextPost from '../../../components/TextPost'; +import UserBar from '../../../components/UserBar'; +import InteractionBar from '../../../components/InteractionBar'; +import { togglePostReaction } from '../../../services/feedService'; +import { useAuth } from '../../../context/AuthContext'; +import { router } from 'expo-router'; + +const { width } = Dimensions.get('window'); type Props = { user: User; @@ -22,6 +34,7 @@ type Props = { type Post = components['schemas']['Post']; const PostsList = ({ user, userId }: Props) => { + const { user: currentUser } = useAuth(); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -53,6 +66,7 @@ const PostsList = ({ user, userId }: Props) => { setError(null); const res = await getPosts({ userId: resolvedUserId, + currentUserId: currentUser?.id, // Pass current user ID to get their reactions repostedPostId: null, // Only show original posts, not reposts limit: 50, }); @@ -63,7 +77,7 @@ const PostsList = ({ user, userId }: Props) => { } finally { setLoading(false); } - }, [resolvedUserId, isValidUuid]); + }, [resolvedUserId, isValidUuid, currentUser?.id]); useEffect(() => { loadPosts(); @@ -112,6 +126,84 @@ const PostsList = ({ user, userId }: Props) => { ); } + const formatDate = (dateString: string | Date) => { + const date = + typeof dateString === 'string' ? new Date(dateString) : dateString; + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + const handlePostPress = (post: Post) => { + router.push(`/postDetail/${post.id}`); + }; + + const handleComment = (post: Post) => { + router.push(`/postDetail/${post.id}`); + }; + + const handleReaction = async (post: Post, reactionIndex: number) => { + if (!currentUser?.id) return; + + const reactionTypes: Array< + 'SPICY' | 'STAR_STUDDED' | 'THOUGHT_PROVOKING' | 'BLOCKBUSTER' + > = ['SPICY', 'STAR_STUDDED', 'THOUGHT_PROVOKING', 'BLOCKBUSTER']; + const reactionType = reactionTypes[reactionIndex]; + + try { + // Optimistically update UI + setPosts(prevPosts => + prevPosts.map(p => { + if (p.id !== post.id) return p; + + const wasSelected = p.userReactions?.includes(reactionType) || false; + + const newCounts = { + SPICY: p.reactionCounts?.SPICY ?? 0, + STAR_STUDDED: p.reactionCounts?.STAR_STUDDED ?? 0, + THOUGHT_PROVOKING: p.reactionCounts?.THOUGHT_PROVOKING ?? 0, + BLOCKBUSTER: p.reactionCounts?.BLOCKBUSTER ?? 0, + }; + newCounts[reactionType] = Math.max( + 0, + newCounts[reactionType] + (wasSelected ? -1 : 1) + ); + + let newUserReactions = [...(p.userReactions || [])]; + if (wasSelected) { + newUserReactions = newUserReactions.filter(r => r !== reactionType); + } else { + newUserReactions.push(reactionType); + } + + return { + ...p, + reactionCounts: newCounts, + userReactions: newUserReactions, + }; + }) + ); + + await togglePostReaction(post.id, currentUser.id, reactionType); + } catch (err) { + console.error('Error toggling reaction:', err); + await loadPosts(); + } + }; + + // Render a horizontal line between posts + const renderSeparator = () => ( + + ); + return ( { maxToRenderPerBatch={posts.length || 10} windowSize={Math.max(5, posts.length || 5)} showsVerticalScrollIndicator={false} - renderItem={({ item: p }) => { - const likeCount = formatCount(p.reactionCount ?? 0); - const commentCount = formatCount(p.commentCount ?? 0); - const repostCount = formatCount(p.repostCount ?? 0); + ItemSeparatorComponent={renderSeparator} + renderItem={({ item: post }) => { + const username = post.UserProfile?.username || user.username || 'User'; const avatar = user.profilePic; - const displayName = - p.UserProfile?.username?.trim() || - user.name || - user.username || - 'User'; - return ( - - - - {displayName} - {p.content} - - - - - {likeCount} - - - - - - {commentCount} - + const hasImage = post.imageUrls && post.imageUrls.length > 0; + const hasStars = post.stars !== null && post.stars !== undefined; + const containsSpoilers = post.spoiler || false; + const movieTitle = post.movie?.title || 'Unknown Movie'; + const movieImagePath = post.movie?.imageUrl; + + // Build reaction data from post + const reactions = [ + { + emoji: '🌶️', + count: post.reactionCounts?.SPICY || 0, + selected: post.userReactions?.includes('SPICY') || false, + }, + { + emoji: '✨', + count: post.reactionCounts?.STAR_STUDDED || 0, + selected: post.userReactions?.includes('STAR_STUDDED') || false, + }, + { + emoji: '🧠', + count: post.reactionCounts?.THOUGHT_PROVOKING || 0, + selected: + post.userReactions?.includes('THOUGHT_PROVOKING') || false, + }, + { + emoji: '🧨', + count: post.reactionCounts?.BLOCKBUSTER || 0, + selected: post.userReactions?.includes('BLOCKBUSTER') || false, + }, + ]; + + // Determine which component to use: + // - ReviewPost for LONG posts with stars + // - PicturePost for SHORT posts with images + // - TextPost for SHORT posts without images + + if (hasStars) { + // LONG post with rating - use ReviewPost with wrapper + return ( + + + + + Check out this new review that I just dropped! + + handlePostPress(post)} + spoiler={containsSpoilers} + /> + + handleComment(post)} + onReactionPress={index => handleReaction(post, index)} + /> - - - - {repostCount} - + + + ); + } else if (hasImage) { + // SHORT post with images - use PicturePost + return ( + + + handlePostPress(post)} + > + + + + handleComment(post)} + onReactionPress={index => handleReaction(post, index)} + /> - - + + + ); + } else { + // SHORT post without images - use TextPost + return ( + + + handlePostPress(post)} + > + + + + handleComment(post)} + onReactionPress={index => handleReaction(post, index)} + /> - - - ); + + ); + } }} ListFooterComponent={} /> ); }; +const styles = StyleSheet.create({ + ratingContainer: { + backgroundColor: '#FFF', + paddingHorizontal: width * 0.04, + paddingTop: width * 0.04, + paddingBottom: width * 0.04, + }, + shareText: { + fontSize: width * 0.04, + color: '#000', + marginTop: width * 0.03, + marginBottom: width * 0.04, + }, + postContainer: { + backgroundColor: '#FFF', + paddingHorizontal: width * 0.04, + paddingTop: width * 0.04, + paddingBottom: width * 0.04, + }, + interactionWrapper: { + marginTop: width * 0.04, + }, +}); + export default PostsList; diff --git a/frontend/components/ImageCropper.tsx b/frontend/components/ImageCropper.tsx new file mode 100644 index 0000000..0d70a9c --- /dev/null +++ b/frontend/components/ImageCropper.tsx @@ -0,0 +1,247 @@ +import { useState } from 'react'; +import { + View, + Image, + TouchableOpacity, + Text, + StyleSheet, + Dimensions, + Modal, + Alert, +} from 'react-native'; +import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; +import { MaterialIcons } from '@expo/vector-icons'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CROP_SIZE = SCREEN_WIDTH * 0.8; + +interface ImageCropperProps { + visible: boolean; + imageUri: string; + onCropComplete: (croppedUri: string) => void; + onCancel: () => void; + aspectRatio?: [number, number]; + circular?: boolean; +} + +export default function ImageCropper({ + visible, + imageUri, + onCropComplete, + onCancel, + aspectRatio = [1, 1], + circular = true, +}: ImageCropperProps) { + const [scale, setScale] = useState(1); + const [processing, setProcessing] = useState(false); + + const handleCrop = async () => { + try { + setProcessing(true); + + // Crop and resize the image + const manipulatedImage = await manipulateAsync( + imageUri, + [ + { resize: { width: CROP_SIZE * scale } }, + ], + { compress: 0.8, format: SaveFormat.JPEG } + ); + + onCropComplete(manipulatedImage.uri); + } catch (error) { + console.error('Error cropping image:', error); + Alert.alert('Error', 'Failed to crop image. Please try again.'); + } finally { + setProcessing(false); + } + }; + + const handleZoomIn = () => { + setScale(prev => Math.min(prev + 0.1, 3)); + }; + + const handleZoomOut = () => { + setScale(prev => Math.max(prev - 0.1, 0.5)); + }; + + return ( + + + {/* Header */} + + + Cancel + + Adjust Photo + + + {processing ? 'Processing...' : 'Done'} + + + + + {/* Image Preview with Circular Overlay */} + + + + {/* Circular Crop Overlay */} + {circular && ( + + + + )} + + + {/* Zoom Controls */} + + + + + + + + + + + + + + + + + + + Pinch to zoom • Drag to reposition + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 20, + paddingTop: 60, + paddingBottom: 20, + }, + cancelText: { + color: '#fff', + fontSize: 16, + }, + title: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + }, + doneText: { + color: '#4A90E2', + fontSize: 16, + fontWeight: '600', + }, + disabled: { + opacity: 0.5, + }, + imageContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + }, + image: { + width: SCREEN_WIDTH, + height: SCREEN_WIDTH, + }, + overlayContainer: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + }, + cropCircle: { + width: CROP_SIZE, + height: CROP_SIZE, + borderRadius: CROP_SIZE / 2, + borderWidth: 2, + borderColor: '#fff', + backgroundColor: 'transparent', + shadowColor: '#000', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.8, + shadowRadius: 20, + }, + controls: { + paddingHorizontal: 20, + paddingBottom: 40, + }, + zoomContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 15, + }, + zoomButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + justifyContent: 'center', + alignItems: 'center', + }, + scaleIndicator: { + flex: 1, + }, + scaleTrack: { + height: 4, + backgroundColor: 'rgba(255, 255, 255, 0.3)', + borderRadius: 2, + position: 'relative', + }, + scaleThumb: { + position: 'absolute', + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: '#fff', + top: -8, + marginLeft: -10, + }, + instructionText: { + color: '#fff', + fontSize: 14, + textAlign: 'center', + marginTop: 15, + opacity: 0.7, + }, +}); diff --git a/frontend/components/LongPostForm.tsx b/frontend/components/LongPostForm.tsx index 1617109..7c66182 100644 --- a/frontend/components/LongPostForm.tsx +++ b/frontend/components/LongPostForm.tsx @@ -5,7 +5,12 @@ import { TextInput, StyleSheet, TouchableOpacity, + Image, + ScrollView, + Alert, } from 'react-native'; +import * as ExpoImagePicker from 'expo-image-picker'; +import { MaterialIcons } from '@expo/vector-icons'; import MovieSelectorModal from './MovieSelectorModal'; import StarRating from './StarRating'; @@ -38,9 +43,60 @@ const LongPostForm = forwardRef( const [tagModalVisible, setTagModalVisible] = useState(false); const [selectedTags, setSelectedTags] = useState([]); const [rating, setRating] = useState(0); + const [selectedImages, setSelectedImages] = useState([]); const CHAR_LIMIT = 280; + 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 () => { + const hasPermission = await requestPermissions(); + if (!hasPermission) return; + + try { + const result = await ExpoImagePicker.launchImageLibraryAsync({ + mediaTypes: ExpoImagePicker.MediaTypeOptions.Images, + allowsMultipleSelection: true, + quality: 0.8, + }); + + if (!result.canceled && result.assets) { + const newImageUris = result.assets.map(asset => asset.uri); + setSelectedImages(prev => [...prev, ...newImageUris]); + } + } catch (error) { + console.error('Error picking image:', error); + Alert.alert('Error', 'Failed to pick image.'); + } + }; + + const removeImage = (indexToRemove: number) => { + setSelectedImages(prev => + prev.filter((_, index) => index !== indexToRemove) + ); + }; + + const handleToolbarAction = (action: string) => { + if (action === 'video') { + pickImage(); + } else { + onToolbarAction(action); + } + }; + useImperativeHandle(ref, () => ({ submit() { if (!movie) { @@ -59,6 +115,7 @@ const LongPostForm = forwardRef( content, rating, tags: selectedTags, + imageUrls: selectedImages.length > 0 ? selectedImages : undefined, }); }, })); @@ -125,7 +182,27 @@ const LongPostForm = forwardRef( ))} - + {selectedImages.length > 0 && ( + + {selectedImages.map((uri, index) => ( + + + removeImage(index)} + > + + + + ))} + + )} + + void; + currentImage?: string | null; + size?: 'small' | 'medium' | 'large'; + shape?: 'circle' | 'square'; + enableCropping?: boolean; // enable/disable cropper +} + +export default function ImagePicker({ + onImageSelected, + currentImage, + size = 'medium', + shape = 'square', + enableCropping = true, +}: ImagePickerProps) { + const [selectedImage, setSelectedImage] = useState(currentImage || null); + const [tempImage, setTempImage] = useState(null); + const [showCropper, setShowCropper] = useState(false); + + 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: !enableCropping, + aspect: shape === 'circle' ? [1, 1] : [4, 3], + quality: enableCropping ? 1 : 0.8, + }); + + if (!result.canceled && result.assets[0]) { + const imageUri = result.assets[0].uri; + + if (enableCropping) { + // show cropper + setTempImage(imageUri); + setShowCropper(true); + } else { + setSelectedImage(imageUri); + if (onImageSelected) { + onImageSelected(imageUri); + } + } + } + } catch (error) { + console.error('Error picking image:', error); + Alert.alert('Error', 'Failed to pick image.'); + } + }; + + // finish crop + const handleCropComplete = (croppedUri: string) => { + setSelectedImage(croppedUri); + if (onImageSelected) { + onImageSelected(croppedUri); + } + setShowCropper(false); + setTempImage(null); + }; + + // cancel crop + const handleCropCancel = () => { + setShowCropper(false); + setTempImage(null); + }; + + // remove + 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 && ( + + + + )} + + {tempImage && enableCropping && ( + + )} + + ); +} + +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 new file mode 100644 index 0000000..eda8d6b --- /dev/null +++ b/frontend/components/PostForm.tsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { View, TextInput, Button, StyleSheet } from 'react-native'; +import ImagePicker from './PhotoPicker'; + + +interface PostFormProps { + showTitle?: boolean; + showStars?: boolean; + showTextBox?: boolean; + 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, imageUri: imageUri || undefined }); + }; + + const handleImageSelected = (uri: string) => { + setImageUri(uri || null); + }; + + return ( + + {showTitle && ( + + )} + + {showStars && ( + + setRating(Number(text))} + keyboardType="numeric" + style={{ + borderWidth: 1, + borderColor: '#ddd', + padding: 10, + borderRadius: 8 + }} + /> + + )} + + {showTextBox && ( + + )} + + {showImagePicker && ( + + + + )} + +