image picker, form

This commit is contained in:
echo committed 2025-02-10 20:28:38 +01:00
1 parent 7f702c58aa
commit f3007d6419
14 files changed
+757 -303

No files matched your search

+150
View File
@@ -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
View File
@@ -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);
}
+63
View File
@@ -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
View File
@@ -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;