image picker fix, working order

This commit is contained in:
dimitar 2025-02-12 12:53:14 +01:00
parent 3aeffb3585
commit 443c48cb01
8 changed files with 143 additions and 44 deletions

4
.env
View File

@ -4,3 +4,7 @@ EXPO_PUBLIC_APPWRITE_DATABASE_ID=677bd04b002bbdbe424f
EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID=679289990026ad25c429 EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID=679289990026ad25c429
EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID=67aa22430023baf8dfcb EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID=67aa22430023baf8dfcb
EXPO_PUBLIC_APPWRITE_BUCKET_ID=67aa2598002da47bee85 EXPO_PUBLIC_APPWRITE_BUCKET_ID=67aa2598002da47bee85
EXPO_PUBLIC_WORLDPAY_API_URL=https://api.worldpay.com/v1
EXPO_PUBLIC_WORLDPAY_KEY=your_worldpay_key_here

View File

@ -1,12 +1,18 @@
import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity } from "react-native"; import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity, Dimensions } from "react-native";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { useLocalSearchParams, router } from "expo-router"; import { useLocalSearchParams, router } from "expo-router";
import icons from "@/constants/icons"; import icons from "@/constants/icons";
import { getCar } from "@/lib/database"; import { getCar } from "@/lib/database";
import { useGlobalContext } from '@/lib/globalProvider';
import Carousel, { ICarouselInstance } from 'react-native-reanimated-carousel';
const CarDetails = () => { const CarDetails = () => {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [carData, setCarData] = useState<any>(null); const [carData, setCarData] = useState<any>(null);
const [activeIndex, setActiveIndex] = useState(0);
const { user } = useGlobalContext();
const width = Dimensions.get('window').width;
const carouselRef = useRef<ICarouselInstance>(null);
useEffect(() => { useEffect(() => {
const fetchCar = async () => { const fetchCar = async () => {
@ -30,24 +36,64 @@ const CarDetails = () => {
contentContainerClassName="pb-32" contentContainerClassName="pb-32"
> >
{/* Header */} {/* Header */}
<View className="relative"> <View className="relative h-72">
<Image <Carousel
source={{ uri: carData.images?.[0] }} ref={carouselRef}
className="w-full h-72" loop
resizeMode="cover" width={width}
height={288}
data={carData.images || []}
scrollAnimationDuration={1000}
onProgressChange={(_, absoluteProgress) => {
setActiveIndex(Math.round(absoluteProgress));
}}
renderItem={({ item }) => (
<Image
source={{ uri: item as string }}
className="w-full h-full"
resizeMode="cover"
/>
)}
/> />
{/* Navigation Arrows */}
{carData.images?.length > 1 && (
<>
<TouchableOpacity
onPress={() => carouselRef.current?.scrollTo ({ index: activeIndex - 1 })}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/50 rounded-full p-2 z-10"
>
<Image
source={icons.backArrow}
className="w-6 h-6"
tintColor="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => carouselRef.current?.scrollTo({ index: activeIndex + 1 })}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black/50 rounded-full p-2 z-10"
>
<Image
source={icons.backArrow}
className="w-6 h-6 rotate-180"
tintColor="white"
/>
</TouchableOpacity>
</>
)}
{/* Back Button */} {/* Back Button */}
<TouchableOpacity <TouchableOpacity
onPress={() => router.back()} onPress={() => router.back()}
className="absolute top-5 left-5 bg-white/90 p-2 rounded-full" className="absolute top-5 left-5 bg-white/90 p-2 rounded-full z-10"
> >
<Image source={icons.backArrow} className="size-6" /> <Image source={icons.backArrow} className="size-6" />
</TouchableOpacity> </TouchableOpacity>
{/* Favorite Button */} {/* Favorite Button */}
<TouchableOpacity <TouchableOpacity
className="absolute top-5 right-5 bg-white/90 p-2 rounded-full" className="absolute top-5 right-5 bg-white/90 p-2 rounded-full z-10"
> >
<Image source={icons.heart} className="size-6" /> <Image source={icons.heart} className="size-6" />
</TouchableOpacity> </TouchableOpacity>

View File

@ -33,12 +33,13 @@ export const CarForm = () => {
price: Number(formData.price), price: Number(formData.price),
year: Number(formData.year), year: Number(formData.year),
images: images, images: images,
postedBy: user.$id postedBy: user.$id,
featured: false
}); });
router.back(); router.back();
} catch (error) { } catch (error) {
console.error('Error creating car:', error); console.error('Error creating car listing:', error);
alert('Failed to create car listing'); alert('Failed to create car listing');
} }
}; };
@ -47,15 +48,11 @@ export const CarForm = () => {
<ScrollView className="p-4"> <ScrollView className="p-4">
<Text className="text-lg font-bold mb-4">Add New Car</Text> <Text className="text-lg font-bold mb-4">Add New Car</Text>
{/* Images */}
<View className="mb-4"> <View className="mb-4">
<ImagePickerComponent <ImagePickerComponent
onImageSelected={(url) => setImages([...images, url])} onImageSelected={setImages}
multiple images={images}
/> />
<Text className="text-sm text-gray-500 mt-2">
{images.length} images selected
</Text>
</View> </View>
{/* Basic Info */} {/* Basic Info */}
@ -115,7 +112,7 @@ export const CarForm = () => {
/> />
</View> </View>
{/* Fuel Type Picker */} {/* Pickers */}
<View className="border border-gray-300 rounded-lg mb-4 px-2"> <View className="border border-gray-300 rounded-lg mb-4 px-2">
<Picker <Picker
selectedValue={formData.fuelType} selectedValue={formData.fuelType}
@ -127,7 +124,6 @@ export const CarForm = () => {
</Picker> </Picker>
</View> </View>
{/* Transmission Picker */}
<View className="border border-gray-300 rounded-lg mb-4 px-2"> <View className="border border-gray-300 rounded-lg mb-4 px-2">
<Picker <Picker
selectedValue={formData.transmission} selectedValue={formData.transmission}
@ -139,6 +135,7 @@ export const CarForm = () => {
</Picker> </Picker>
</View> </View>
{/* Submit Button */}
<TouchableOpacity <TouchableOpacity
onPress={handleSubmit} onPress={handleSubmit}
className="bg-blue-500 p-4 rounded-full" className="bg-blue-500 p-4 rounded-full"

View File

@ -1,16 +1,21 @@
import React from 'react'; import React, { useState } from 'react';
import { View, TouchableOpacity, Image, Text } from 'react-native'; import { View, TouchableOpacity, Image, Text, ScrollView } from 'react-native';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { uploadImage } from '@/lib/appwrite'; import { uploadImage } from '@/lib/appwrite';
import icons from '@/constants/icons'; import icons from '@/constants/icons';
interface Props { interface Props {
onImageSelected: (url: string) => void; onImageSelected: (url: string[]) => void;
multiple?: boolean; images: string[];
} }
export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Props) => { export const ImagePickerComponent = ({ onImageSelected, images }: Props) => {
const pickImage = async () => { const pickImage = async () => {
if (images.length >= 5) {
alert('Maximum 5 images allowed');
return;
}
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {
@ -19,27 +24,24 @@ export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Prop
} }
const result = await ImagePicker.launchImageLibraryAsync({ const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, mediaTypes: ['images'],
allowsEditing: true, allowsEditing: true,
aspect: [4, 3], aspect: [4, 3],
quality: 1, quality: 1,
allowsMultipleSelection: multiple,
}); });
if (!result.canceled) { if (!result.canceled) {
try { try {
// Convert image to blob
const response = await fetch(result.assets[0].uri); const response = await fetch(result.assets[0].uri);
const blob = await response.blob(); const blob = await response.blob();
// Upload to Appwrite
const imageUrl = await uploadImage({ const imageUrl = await uploadImage({
uri: result.assets[0].uri, uri: result.assets[0].uri,
type: 'image/jpeg', type: 'image/jpeg',
name: 'image.jpg', name: 'image.jpg',
size: blob.size size: blob.size
}); });
onImageSelected(imageUrl); onImageSelected([...images, imageUrl]);
} catch (error) { } catch (error) {
console.error('Error uploading image:', error); console.error('Error uploading image:', error);
alert('Failed to upload image'); alert('Failed to upload image');
@ -47,17 +49,49 @@ export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Prop
} }
}; };
const removeImage = (index: number) => {
const newImages = [...images];
newImages.splice(index, 1);
onImageSelected(newImages);
};
return ( return (
<TouchableOpacity <View>
onPress={pickImage} <ScrollView
className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-4" horizontal
> showsHorizontalScrollIndicator={false}
<Image className="mb-4"
source={icons.dumbell} >
className="w-8 h-8 mb-2" {images.map((image, index) => (
/> <View key={index} className="relative mr-2">
{/* change dumbell to camera */} <Image
<Text className="text-gray-500">Upload Image</Text> source={{ uri: image }}
</TouchableOpacity> className="w-24 h-24 rounded-lg"
/>
<TouchableOpacity
onPress={() => removeImage(index)}
className="absolute -top-2 -right-2 bg-red-500 rounded-full p-1"
>
<Text className="text-white text-xs"></Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
{images.length < 5 && (
<TouchableOpacity
onPress={pickImage}
className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-4"
>
<Image
source={icons.dumbell}
className="w-8 h-8 mb-2"
/>
<Text className="text-gray-500">
Upload Image ({images.length}/5)
</Text>
</TouchableOpacity>
)}
</View>
); );
}; };

View File

@ -23,6 +23,8 @@ export const config = {
usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID, usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID,
carId: process.env.EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID, carId: process.env.EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID,
bucketId: process.env.EXPO_PUBLIC_APPWRITE_BUCKET_ID, bucketId: process.env.EXPO_PUBLIC_APPWRITE_BUCKET_ID,
worldpayApiUrl: process.env.NODE_ENV === 'development' ? 'https://api.worldpay.com/v1' : process.env.EXPO_PUBLIC_WORLDPAY_API_URL,
worldpayKey: process.env.NODE_ENV === 'development' ? 'your_worldpay_key_here' : process.env.EXPO_PUBLIC_WORLDPAY_KEY,
} }
export const client = new Client(); export const client = new Client();

View File

@ -1,2 +1,4 @@
splash screen not showing splash screen not showing
database seed 2:32
## next to implement
in-app payment, develop on separate branch

13
package-lock.json generated
View File

@ -32,6 +32,7 @@
"react-native-appwrite": "^0.6.0", "react-native-appwrite": "^0.6.0",
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-reanimated-carousel": "^4.0.2",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
@ -15092,6 +15093,18 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-reanimated-carousel": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/react-native-reanimated-carousel/-/react-native-reanimated-carousel-4.0.2.tgz",
"integrity": "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.70.3",
"react-native-gesture-handler": ">=2.9.0",
"react-native-reanimated": ">=3.0.0"
}
},
"node_modules/react-native-safe-area-context": { "node_modules/react-native-safe-area-context": {
"version": "4.12.0", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",

View File

@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.0.2", "@expo/vector-icons": "^14.0.2",
"@react-native-picker/picker": "2.9.0",
"@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14", "@react-navigation/native": "^7.0.14",
"expo": "~52.0.23", "expo": "~52.0.23",
@ -23,6 +24,7 @@
"expo-constants": "~17.0.3", "expo-constants": "~17.0.3",
"expo-font": "~13.0.2", "expo-font": "~13.0.2",
"expo-haptics": "~14.0.0", "expo-haptics": "~14.0.0",
"expo-image-picker": "~16.0.5",
"expo-linking": "~7.0.3", "expo-linking": "~7.0.3",
"expo-router": "~4.0.15", "expo-router": "~4.0.15",
"expo-splash-screen": "~0.29.18", "expo-splash-screen": "~0.29.18",
@ -37,15 +39,14 @@
"react-native-appwrite": "^0.6.0", "react-native-appwrite": "^0.6.0",
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-reanimated-carousel": "^4.0.2",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.19.13", "react-native-web": "~0.19.13",
"react-native-webview": "13.12.5", "react-native-webview": "13.12.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4"
"expo-image-picker": "~16.0.5",
"@react-native-picker/picker": "2.9.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",