image picker, form
This commit is contained in:
1 parent
7f702c58aa
commit
f3007d6419
14 files changed
+757
-303
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { ImagePickerComponent } from './ImagePicker';
|
||||
import { createCar } from '@/lib/database';
|
||||
import { useGlobalContext } from '@/lib/globalProvider';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
const FUEL_TYPES = ['Petrol', 'Diesel', 'Electric', 'Hybrid'] as const;
|
||||
const TRANSMISSIONS = ['Manual', 'Automatic'] as const;
|
||||
|
||||
export const CarForm = () => {
|
||||
const { user } = useGlobalContext();
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
price: '',
|
||||
make: '',
|
||||
model: '',
|
||||
year: '',
|
||||
fuelType: 'Petrol' as const,
|
||||
transmission: 'Automatic' as const,
|
||||
location: ''
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!user) throw new Error('User not authenticated');
|
||||
|
||||
await createCar({
|
||||
...formData,
|
||||
price: Number(formData.price),
|
||||
year: Number(formData.year),
|
||||
images: images,
|
||||
postedBy: user.$id
|
||||
});
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error('Error creating car:', error);
|
||||
alert('Failed to create car listing');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">Add New Car</Text>
|
||||
|
||||
{/* Images */}
|
||||
<View className="mb-4">
|
||||
<ImagePickerComponent
|
||||
onImageSelected={(url) => setImages([...images, url])}
|
||||
multiple
|
||||
/>
|
||||
<Text className="text-sm text-gray-500 mt-2">
|
||||
{images.length} images selected
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Basic Info */}
|
||||
<TextInput
|
||||
className="border border-gray-300 rounded-lg p-2 mb-4"
|
||||
placeholder="Title"
|
||||
value={formData.title}
|
||||
onChangeText={(text) => setFormData({...formData, title: text})}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="border border-gray-300 rounded-lg p-2 mb-4"
|
||||
placeholder="Description"
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
value={formData.description}
|
||||
onChangeText={(text) => setFormData({...formData, description: text})}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="border border-gray-300 rounded-lg p-2 mb-4"
|
||||
placeholder="Price"
|
||||
keyboardType="numeric"
|
||||
value={formData.price}
|
||||
onChangeText={(text) => setFormData({...formData, price: text})}
|
||||
/>
|
||||
|
||||
{/* Car Details */}
|
||||
<View className="flex-row gap-2 mb-4">
|
||||
<TextInput
|
||||
className="flex-1 border border-gray-300 rounded-lg p-2"
|
||||
placeholder="Make"
|
||||
value={formData.make}
|
||||
onChangeText={(text) => setFormData({...formData, make: text})}
|
||||
/>
|
||||
<TextInput
|
||||
className="flex-1 border border-gray-300 rounded-lg p-2"
|
||||
placeholder="Model"
|
||||
value={formData.model}
|
||||
onChangeText={(text) => setFormData({...formData, model: text})}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex-row gap-2 mb-4">
|
||||
<TextInput
|
||||
className="flex-1 border border-gray-300 rounded-lg p-2"
|
||||
placeholder="Year"
|
||||
keyboardType="numeric"
|
||||
value={formData.year}
|
||||
onChangeText={(text) => setFormData({...formData, year: text})}
|
||||
/>
|
||||
<TextInput
|
||||
className="flex-1 border border-gray-300 rounded-lg p-2"
|
||||
placeholder="Location"
|
||||
value={formData.location}
|
||||
onChangeText={(text) => setFormData({...formData, location: text})}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Fuel Type Picker */}
|
||||
<View className="border border-gray-300 rounded-lg mb-4 px-2">
|
||||
<Picker
|
||||
selectedValue={formData.fuelType}
|
||||
onValueChange={(value) => setFormData({...formData, fuelType: value})}
|
||||
>
|
||||
{FUEL_TYPES.map((type) => (
|
||||
<Picker.Item key={type} label={type} value={type} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
{/* Transmission Picker */}
|
||||
<View className="border border-gray-300 rounded-lg mb-4 px-2">
|
||||
<Picker
|
||||
selectedValue={formData.transmission}
|
||||
onValueChange={(value) => setFormData({...formData, transmission: value})}
|
||||
>
|
||||
{TRANSMISSIONS.map((type) => (
|
||||
<Picker.Item key={type} label={type} value={type} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit}
|
||||
className="bg-blue-500 p-4 rounded-full"
|
||||
>
|
||||
<Text className="text-white text-center font-semibold">Add Car</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
+57
-23
@@ -2,30 +2,54 @@ import {View, Text, TouchableOpacity, Image} from 'react-native'
|
||||
import React from 'react'
|
||||
import images from "@/constants/images";
|
||||
import icons from "@/constants/icons";
|
||||
import { router } from "expo-router";
|
||||
import { cars, featuredCars } from "@/constants/data";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
|
||||
|
||||
interface Props {
|
||||
onPress?: () => void
|
||||
onPress?: () => void;
|
||||
id?: string;
|
||||
data: {
|
||||
id?: string;
|
||||
$id?: string;
|
||||
title: string;
|
||||
location: string;
|
||||
price: string | number;
|
||||
image?: string;
|
||||
images?: string[];
|
||||
make?: string;
|
||||
model?: string;
|
||||
year: string | number;
|
||||
fuelType: string;
|
||||
transmission: string;
|
||||
mileage?: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
postedBy?: string;
|
||||
createdAt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const FeaturedCard = ({onPress} :Props) => {
|
||||
export const FeaturedCard = ({data} :Props) => {
|
||||
return (
|
||||
<TouchableOpacity className={'flex flex-col items-start w-60 h-60 relative'}>
|
||||
<Image source={images.car1} className={'w-60 h-40 rounded-2xl'} resizeMode={"contain"}/>
|
||||
<Image source={images.cardGradient} className={'size-full rounded-2xl absolute bottom-0'}/>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push(`/cars/${data.$id || data.id}`)}
|
||||
className={'flex flex-col items-start w-60 h-60 relative'}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: data.images?.[0] || data.image }}
|
||||
className={'w-60 h-40 rounded-2xl'}
|
||||
resizeMode={"cover"}
|
||||
/>
|
||||
<Image source={images.cardGradient} className={'size-full rounded-2xl absolute bottom-0'}/>
|
||||
|
||||
<View className={'flex flex-row items-center bg-white/90 px-3 py-1.5 rounded-full absolute top-5 right-1'}>
|
||||
<Image source={icons.star} className={'size-3'}/>
|
||||
<Text className={'text-sm ml-1'}>5</Text>
|
||||
</View>
|
||||
<View className={'flex flex-col items-start absolute bottom-1 inset-x-5'}>
|
||||
<Text className={'text-white text-base font-bold'} numberOfLines={1}>Honda - xyz</Text>
|
||||
<Text className={'text-white'}>Skopje</Text>
|
||||
<Text className={'text-white text-base font-bold'} numberOfLines={1}>{data.title}</Text>
|
||||
<Text className={'text-white'}>{data.location}</Text>
|
||||
|
||||
<View className={'flex flex-row items-center justify-between w-full'}>
|
||||
<Text className={'text-base text-white'}>
|
||||
25.000
|
||||
</Text>
|
||||
<Text className={'text-base text-white'}>€{data.price}</Text>
|
||||
<Image source={icons.heart} className={'size-5'}/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -33,18 +57,28 @@ export const FeaturedCard = ({onPress} :Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const Card = ({onPress} :Props) => {
|
||||
|
||||
export const Card = ({data} :Props) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} className={'flex-1 w-full mt-4 px-3 py-4 rounded-xl bg-white shadow-lg shadow-black-100/70 relative'}>
|
||||
<View className={'flex flex-row items-center absolute px-2 top-5 right-5 bg-white/90 p-1 rounded-full z-50'}>
|
||||
<Image source={icons.star} className={'size-2.5'}/>
|
||||
<Text className={'text-sm ml-1'}>5.0</Text>
|
||||
</View>
|
||||
<Image source={images.car2} className={'w-full h-40 rounded-lg'}/>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push(`/cars/${data.$id || data.id}`)}
|
||||
className={'flex-1 w-full mt-4 px-3 py-4 rounded-xl bg-white shadow-lg shadow-black-100/70 relative'}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: data.images?.[0] || data.image }}
|
||||
className={'w-full h-40 rounded-lg'}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<View className={'flex flex-col mt-2'}>
|
||||
<Text className={'text-base text-gray-600'}>xyz</Text>
|
||||
<Text className={'text-sm'}>Skopje</Text>
|
||||
<Text className={'text-base text-gray-600'}>{data?.title}</Text>
|
||||
<Text className={'text-sm'}>{data?.location}</Text>
|
||||
<Text className={'text-base text-blue-500'}>€{data?.price}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const CarDetails = () => {
|
||||
const { id } = useLocalSearchParams();
|
||||
const carData = [...cars, ...featuredCars].find(car => car.id === id);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Image, Text } from 'react-native';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { uploadImage } from '@/lib/appwrite';
|
||||
import icons from '@/constants/icons';
|
||||
|
||||
interface Props {
|
||||
onImageSelected: (url: string) => void;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export const ImagePickerComponent = ({ onImageSelected, multiple = false }: Props) => {
|
||||
const pickImage = async () => {
|
||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (status !== 'granted') {
|
||||
alert('Sorry, we need camera roll permissions to make this work!');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.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);
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
alert('Failed to upload image');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
{/* change dumbell to camera */}
|
||||
<Text className="text-gray-500">Upload Image</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
+18
-38
@@ -1,41 +1,21 @@
|
||||
import {View, Text, TextInput, SafeAreaView, Image, TouchableOpacity} from 'react-native'
|
||||
import React, {useState} from 'react'
|
||||
import {router, useLocalSearchParams, usePathname} from "expo-router";
|
||||
import icons from "@/constants/icons";
|
||||
import {useDebouncedCallback} from "use-debounce";
|
||||
import React from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Image } from 'react-native';
|
||||
import icons from '@/constants/icons';
|
||||
|
||||
const Search = () => {
|
||||
const path = usePathname();
|
||||
const params = useLocalSearchParams<{query?: string}>();
|
||||
const [search, setSearch] = useState(params.query);
|
||||
return (
|
||||
<View className="flex-row items-center bg-gray-100 rounded-full px-4 py-2">
|
||||
<Image source={icons.search} className="w-5 h-5 mr-2" />
|
||||
<TextInput
|
||||
placeholder="Search cars..."
|
||||
className="flex-1"
|
||||
placeholderTextColor="#666"
|
||||
/>
|
||||
<TouchableOpacity>
|
||||
<Image source={icons.filter} className="w-5 h-5" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// we will optimize search with debounce
|
||||
|
||||
const debounceSearch = useDebouncedCallback(
|
||||
(text: string) => router.setParams({ query: text }),
|
||||
300 // debounce delay in milliseconds
|
||||
);
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
console.log(search)
|
||||
setSearch(text);
|
||||
debounceSearch(text)
|
||||
}
|
||||
return (
|
||||
<View className={'flex flex-row items-center justify-between w-full px-4 rounded-lg border border-blue-200 mt-5 py-2'}>
|
||||
<View className={'flex-1 flex flex-row items-center justify-start z-50'}>
|
||||
<Image source={icons.search} className={'size-5'}/>
|
||||
<TextInput
|
||||
value={search}
|
||||
onChangeText={handleSearch}
|
||||
placeholder={'Search...'}
|
||||
style={{flex: 1, marginLeft: 10}}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity>
|
||||
<Image source={icons.filter} className={'size-5'}/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
export default Search
|
||||
export default Search;
|
||||
Reference in new issue
Block a user