image picker fix, working order
This commit is contained in:
parent
3aeffb3585
commit
443c48cb01
4
.env
4
.env
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
<Carousel
|
||||||
|
ref={carouselRef}
|
||||||
|
loop
|
||||||
|
width={width}
|
||||||
|
height={288}
|
||||||
|
data={carData.images || []}
|
||||||
|
scrollAnimationDuration={1000}
|
||||||
|
onProgressChange={(_, absoluteProgress) => {
|
||||||
|
setActiveIndex(Math.round(absoluteProgress));
|
||||||
|
}}
|
||||||
|
renderItem={({ item }) => (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: carData.images?.[0] }}
|
source={{ uri: item as string }}
|
||||||
className="w-full h-72"
|
className="w-full h-full"
|
||||||
resizeMode="cover"
|
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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,7 +49,36 @@ export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Prop
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeImage = (index: number) => {
|
||||||
|
const newImages = [...images];
|
||||||
|
newImages.splice(index, 1);
|
||||||
|
onImageSelected(newImages);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<View>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<View key={index} className="relative mr-2">
|
||||||
|
<Image
|
||||||
|
source={{ uri: image }}
|
||||||
|
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
|
<TouchableOpacity
|
||||||
onPress={pickImage}
|
onPress={pickImage}
|
||||||
className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-4"
|
className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-4"
|
||||||
@ -56,8 +87,11 @@ export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Prop
|
|||||||
source={icons.dumbell}
|
source={icons.dumbell}
|
||||||
className="w-8 h-8 mb-2"
|
className="w-8 h-8 mb-2"
|
||||||
/>
|
/>
|
||||||
{/* change dumbell to camera */}
|
<Text className="text-gray-500">
|
||||||
<Text className="text-gray-500">Upload Image</Text>
|
Upload Image ({images.length}/5)
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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();
|
||||||
|
|||||||
4
notes.md
4
notes.md
@ -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
13
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user