diff --git a/.env b/.env index b21e8b6..fe09f56 100644 --- a/.env +++ b/.env @@ -4,3 +4,7 @@ EXPO_PUBLIC_APPWRITE_DATABASE_ID=677bd04b002bbdbe424f EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID=679289990026ad25c429 EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID=67aa22430023baf8dfcb EXPO_PUBLIC_APPWRITE_BUCKET_ID=67aa2598002da47bee85 + + +EXPO_PUBLIC_WORLDPAY_API_URL=https://api.worldpay.com/v1 +EXPO_PUBLIC_WORLDPAY_KEY=your_worldpay_key_here diff --git a/app/(root)/cars/[id].tsx b/app/(root)/cars/[id].tsx index f3c2e8f..c8aaa21 100644 --- a/app/(root)/cars/[id].tsx +++ b/app/(root)/cars/[id].tsx @@ -1,12 +1,18 @@ -import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity } from "react-native"; -import React, { useEffect, useState } from "react"; +import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity, Dimensions } from "react-native"; +import React, { useEffect, useState, useRef } from "react"; import { useLocalSearchParams, router } from "expo-router"; import icons from "@/constants/icons"; import { getCar } from "@/lib/database"; +import { useGlobalContext } from '@/lib/globalProvider'; +import Carousel, { ICarouselInstance } from 'react-native-reanimated-carousel'; const CarDetails = () => { const { id } = useLocalSearchParams(); const [carData, setCarData] = useState(null); + const [activeIndex, setActiveIndex] = useState(0); + const { user } = useGlobalContext(); + const width = Dimensions.get('window').width; + const carouselRef = useRef(null); useEffect(() => { const fetchCar = async () => { @@ -30,24 +36,64 @@ const CarDetails = () => { contentContainerClassName="pb-32" > {/* Header */} - - + { + setActiveIndex(Math.round(absoluteProgress)); + }} + renderItem={({ item }) => ( + + )} /> + {/* Navigation Arrows */} + {carData.images?.length > 1 && ( + <> + 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" + > + + + + 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" + > + + + + )} + {/* Back Button */} 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" > {/* Favorite Button */} diff --git a/components/CarForm.tsx b/components/CarForm.tsx index 25a0c47..8b3224e 100644 --- a/components/CarForm.tsx +++ b/components/CarForm.tsx @@ -33,12 +33,13 @@ export const CarForm = () => { price: Number(formData.price), year: Number(formData.year), images: images, - postedBy: user.$id + postedBy: user.$id, + featured: false }); router.back(); } catch (error) { - console.error('Error creating car:', error); + console.error('Error creating car listing:', error); alert('Failed to create car listing'); } }; @@ -47,15 +48,11 @@ export const CarForm = () => { Add New Car - {/* Images */} setImages([...images, url])} - multiple + onImageSelected={setImages} + images={images} /> - - {images.length} images selected - {/* Basic Info */} @@ -115,7 +112,7 @@ export const CarForm = () => { /> - {/* Fuel Type Picker */} + {/* Pickers */} { - {/* Transmission Picker */} { + {/* Submit Button */} void; - multiple?: boolean; + onImageSelected: (url: string[]) => void; + images: string[]; } -export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Props) => { +export const ImagePickerComponent = ({ onImageSelected, images }: Props) => { const pickImage = async () => { + if (images.length >= 5) { + alert('Maximum 5 images allowed'); + return; + } + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { @@ -19,27 +24,24 @@ export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Prop } const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, + mediaTypes: ['images'], allowsEditing: true, aspect: [4, 3], quality: 1, - allowsMultipleSelection: multiple, }); if (!result.canceled) { try { - // Convert image to blob const response = await fetch(result.assets[0].uri); const blob = await response.blob(); - // Upload to Appwrite const imageUrl = await uploadImage({ uri: result.assets[0].uri, type: 'image/jpeg', name: 'image.jpg', size: blob.size }); - onImageSelected(imageUrl); + onImageSelected([...images, imageUrl]); } catch (error) { console.error('Error uploading image:', error); 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 ( - - - {/* change dumbell to camera */} - Upload Image - + + + {images.map((image, index) => ( + + + removeImage(index)} + className="absolute -top-2 -right-2 bg-red-500 rounded-full p-1" + > + + + + ))} + + + {images.length < 5 && ( + + + + Upload Image ({images.length}/5) + + + )} + ); }; \ No newline at end of file diff --git a/lib/appwrite.ts b/lib/appwrite.ts index e1244bf..9b0106f 100644 --- a/lib/appwrite.ts +++ b/lib/appwrite.ts @@ -23,6 +23,8 @@ export const config = { usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID, carId: process.env.EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_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(); diff --git a/notes.md b/notes.md index 8c1f98b..0311b50 100644 --- a/notes.md +++ b/notes.md @@ -1,2 +1,4 @@ splash screen not showing -database seed 2:32 + +## next to implement +in-app payment, develop on separate branch diff --git a/package-lock.json b/package-lock.json index 372f6f5..e5246ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-native-appwrite": "^0.6.0", "react-native-gesture-handler": "~2.20.2", "react-native-reanimated": "~3.16.1", + "react-native-reanimated-carousel": "^4.0.2", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-url-polyfill": "^2.0.0", @@ -15092,6 +15093,18 @@ "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": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz", diff --git a/package.json b/package.json index 38f6354..677f8de 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@expo/vector-icons": "^14.0.2", + "@react-native-picker/picker": "2.9.0", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "expo": "~52.0.23", @@ -23,6 +24,7 @@ "expo-constants": "~17.0.3", "expo-font": "~13.0.2", "expo-haptics": "~14.0.0", + "expo-image-picker": "~16.0.5", "expo-linking": "~7.0.3", "expo-router": "~4.0.15", "expo-splash-screen": "~0.29.18", @@ -37,15 +39,14 @@ "react-native-appwrite": "^0.6.0", "react-native-gesture-handler": "~2.20.2", "react-native-reanimated": "~3.16.1", + "react-native-reanimated-carousel": "^4.0.2", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-url-polyfill": "^2.0.0", "react-native-web": "~0.19.13", "react-native-webview": "13.12.5", "tailwindcss": "^3.4.17", - "use-debounce": "^10.0.4", - "expo-image-picker": "~16.0.5", - "@react-native-picker/picker": "2.9.0" + "use-debounce": "^10.0.4" }, "devDependencies": { "@babel/core": "^7.25.2",