image picker, form
This commit is contained in:
parent
7f702c58aa
commit
f3007d6419
9
.env
9
.env
@ -1,11 +1,6 @@
|
|||||||
EXPO_PUBLIC_APPWRITE_PROJECT_ID=mobilemk
|
EXPO_PUBLIC_APPWRITE_PROJECT_ID=mobilemk
|
||||||
EXPO_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
EXPO_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||||
EXPO_PUBLIC_APPWRITE_DATABASE_ID=677bd04b002bbdbe424f
|
EXPO_PUBLIC_APPWRITE_DATABASE_ID=677bd04b002bbdbe424f
|
||||||
EXPO_PUBLIC_APPWRITE_OGLASUVAC_COLLECTION_ID=677bd0e000393afd25b9
|
|
||||||
EXPO_PUBLIC_APPWRITE_GALERIES_COLLECTION_ID=6787d073000bc4bdb6cb
|
|
||||||
EXPO_PUBLIC_APPWRITE_REVIEWS_COLLECTION_ID=6787d165001e6877d572
|
|
||||||
EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID=6787d2cb00199e6ea873
|
|
||||||
EXPO_PUBLIC_APPWRITE_VEHICLE_COLLECTION_ID=67892473000c84b108c4
|
|
||||||
EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID=679289990026ad25c429
|
EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID=679289990026ad25c429
|
||||||
|
EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID=67aa22430023baf8dfcb
|
||||||
|
EXPO_PUBLIC_APPWRITE_BUCKET_ID=67aa2598002da47bee85
|
||||||
|
|||||||
@ -7,87 +7,109 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import images from "@/constants/images";
|
// import images from "@/constants/images";
|
||||||
import icons from "@/constants/icons";
|
import icons from "@/constants/icons";
|
||||||
import Search from "@/components/Search";
|
import Search from "@/components/Search";
|
||||||
import { useGlobalContext } from "@/lib/globalProvider";
|
import { useGlobalContext } from "@/lib/globalProvider";
|
||||||
import { Card, FeaturedCard } from "@/components/Cards";
|
import { Card, FeaturedCard } from "@/components/Cards";
|
||||||
import Filters from "@/components/Filters";
|
import Filters from "@/components/Filters";
|
||||||
|
// import { cars, featuredCars } from "@/constants/data";
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getCars, getFeaturedCars } from "@/lib/database";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
// create func that determines the time od day [morning, evening, night]
|
|
||||||
const { user, refetch } = useGlobalContext();
|
const { user, refetch } = useGlobalContext();
|
||||||
|
const [cars, setCars] = useState<any[]>([]);
|
||||||
|
const [featuredCars, setFeaturedCars] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const [allCars, featured] = await Promise.all([
|
||||||
|
getCars(),
|
||||||
|
getFeaturedCars()
|
||||||
|
]);
|
||||||
|
setCars(allCars);
|
||||||
|
setFeaturedCars(featured);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className={"bg-white h-full"}>
|
<SafeAreaView className={"bg-white h-full"}>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={[5, 6, 7, 8]}
|
data={cars}
|
||||||
renderItem={({ item }) => <Card />}
|
renderItem={({ item }) => <Card data={item} />}
|
||||||
keyExtractor={(item) => item.toString()}
|
keyExtractor={(item) => item.$id}
|
||||||
numColumns={2}
|
numColumns={2}
|
||||||
contentContainerClassName={"pb-32"}
|
|
||||||
columnWrapperClassName={"flex flex-row gap-5 px-5"}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<View className={"px-4"}>
|
<View>
|
||||||
<View className={"flex flex-row items-center justify-between mt-5"}>
|
{/* User Info */}
|
||||||
<View className={"flex flex-row items-center"}>
|
<View className="px-5 pt-5 flex-row items-center justify-between">
|
||||||
<Image
|
<View>
|
||||||
source={{ uri: user?.avatar }}
|
<Text className="text-2xl font-bold">Welcome back,</Text>
|
||||||
className={"rounded-full size-12"}
|
<Text className="text-lg text-gray-500">{user?.name}</Text>
|
||||||
/>
|
|
||||||
<View
|
|
||||||
className={"flex flex-col items-start ml-4 justify-center"}
|
|
||||||
>
|
|
||||||
<Text className={"capitalize"}>good morning</Text>
|
|
||||||
<Text className={"capitalize font-bold"}>{user?.name}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<Image source={icons.bell} className={"size-6"} />
|
<TouchableOpacity>
|
||||||
|
<Image
|
||||||
|
source={{ uri: user?.avatar }}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Search />
|
{/* Search */}
|
||||||
|
<View className="px-5 mt-5">
|
||||||
|
<Search />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Featured Section */}
|
||||||
<View className={"my-5"}>
|
<View className={"my-5"}>
|
||||||
<View className={"flex flex-row items-center justify-between"}>
|
<View className={"flex flex-row items-center justify-between"}>
|
||||||
<Text className={"capitalize font-bold text-base"}>
|
<Text className={"capitalize font-bold text-base"}>featured</Text>
|
||||||
featured
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity>
|
<TouchableOpacity>
|
||||||
<Text className={"capitalize text-base text-blue-500"}>
|
<Text className={"capitalize text-base text-blue-500"}>see all</Text>
|
||||||
see all
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={[1, 2, 3]}
|
data={featuredCars}
|
||||||
renderItem={({ item }) => <FeaturedCard />}
|
renderItem={({ item }) => <FeaturedCard data={item} />}
|
||||||
keyExtractor={(item) => item.toString()}
|
keyExtractor={(item) => item.$id}
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
bounces={false}
|
bounces={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerClassName={"flex gap-3 mt-5"}
|
contentContainerClassName={"flex gap-3 mt-5"}
|
||||||
/>
|
/>
|
||||||
{/*<View className={'flex flex-row gap-5 mt-5'}>*/}
|
|
||||||
{/* <FeaturedCard/>*/}
|
|
||||||
{/* <FeaturedCard/>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Latest Cars Section */}
|
||||||
<View className={"flex flex-row items-center justify-between"}>
|
<View className={"flex flex-row items-center justify-between"}>
|
||||||
<Text className={"capitalize font-bold text-base"}>
|
<Text className={"capitalize font-bold text-base"}>latest cars</Text>
|
||||||
latest cars
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity>
|
<TouchableOpacity>
|
||||||
<Text className={"capitalize font-bold"}>see all</Text>
|
<Text className={"capitalize font-bold"}>see all</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Filters />
|
<Filters />
|
||||||
<View className={"flex flex-row gap-5 mt-5"}>
|
|
||||||
<Card />
|
|
||||||
<Card />
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
contentContainerClassName={"pb-32"}
|
||||||
|
columnWrapperClassName={"flex flex-row gap-5 px-5"}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
|
{/* Add Car Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.push('/cars/new')}
|
||||||
|
className="absolute bottom-8 right-8 bg-blue-500 rounded-full p-4 shadow-lg"
|
||||||
|
>
|
||||||
|
<View className="bg-white rounded-full p-2 mb-14">
|
||||||
|
<Image source={icons.bed} className="size-6" tintColor="black" />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,118 @@
|
|||||||
import { View, Text } from "react-native";
|
import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity } from "react-native";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams, router } from "expo-router";
|
||||||
|
import icons from "@/constants/icons";
|
||||||
|
import { getCar } from "@/lib/database";
|
||||||
|
|
||||||
const Car = () => {
|
const CarDetails = () => {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
|
const [carData, setCarData] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCar = async () => {
|
||||||
|
try {
|
||||||
|
const car = await getCar(id as string);
|
||||||
|
setCarData(car);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching car:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCar();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (!carData) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<SafeAreaView className="bg-white flex-1">
|
||||||
<Text>Car {id}</Text>
|
<ScrollView
|
||||||
</View>
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerClassName="pb-32"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View className="relative">
|
||||||
|
<Image
|
||||||
|
source={{ uri: carData.images?.[0] }}
|
||||||
|
className="w-full h-72"
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Back Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
className="absolute top-5 left-5 bg-white/90 p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<Image source={icons.backArrow} className="size-6" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Favorite Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
className="absolute top-5 right-5 bg-white/90 p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<Image source={icons.heart} className="size-6" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View className="px-5 mt-5">
|
||||||
|
{/* Title and Rating */}
|
||||||
|
<View className="flex-row justify-between items-center">
|
||||||
|
<Text className="text-2xl font-bold">{carData.title}</Text>
|
||||||
|
<View className="flex-row items-center bg-white px-3 py-1.5 rounded-full border border-gray-200">
|
||||||
|
<Image source={icons.star} className="size-4" />
|
||||||
|
<Text className="ml-1">{carData.rating}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<View className="flex-row items-center mt-2">
|
||||||
|
<Image source={icons.location} className="size-5" />
|
||||||
|
<Text className="ml-2 text-gray-500">{carData.location}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<Text className="text-2xl font-bold text-blue-500 mt-4">€{carData.price}</Text>
|
||||||
|
|
||||||
|
{/* Details */}
|
||||||
|
<View className="mt-6">
|
||||||
|
<Text className="text-lg font-bold mb-3">Car Details</Text>
|
||||||
|
<View className="flex-row flex-wrap gap-4">
|
||||||
|
<View className="bg-gray-100 px-4 py-2 rounded-lg">
|
||||||
|
<Text>{carData.year}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="bg-gray-100 px-4 py-2 rounded-lg">
|
||||||
|
<Text>{carData.fuelType}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="bg-gray-100 px-4 py-2 rounded-lg">
|
||||||
|
<Text>{carData.transmission}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="bg-gray-100 px-4 py-2 rounded-lg">
|
||||||
|
<Text>{carData.mileage} km</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<View className="mt-6">
|
||||||
|
<Text className="text-lg font-bold mb-3">Description</Text>
|
||||||
|
<Text className="text-gray-600 leading-5">
|
||||||
|
{carData.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Bottom Actions */}
|
||||||
|
<View className="absolute bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-5 flex-row justify-between">
|
||||||
|
<TouchableOpacity className="flex-1 mr-2 bg-white border border-blue-500 rounded-full py-3 items-center">
|
||||||
|
<Text className="text-blue-500 font-semibold">Message</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity className="flex-1 ml-2 bg-blue-500 rounded-full py-3 items-center">
|
||||||
|
<Text className="text-white font-semibold">Call</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Car;
|
export default CarDetails;
|
||||||
|
|||||||
10
app/(root)/cars/new.tsx
Normal file
10
app/(root)/cars/new.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { SafeAreaView } from 'react-native';
|
||||||
|
import { CarForm } from '@/components/CarForm';
|
||||||
|
|
||||||
|
export default function NewCar() {
|
||||||
|
return (
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
<CarForm />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
components/CarForm.tsx
Normal file
150
components/CarForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,30 +2,54 @@ import {View, Text, TouchableOpacity, Image} from 'react-native'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import images from "@/constants/images";
|
import images from "@/constants/images";
|
||||||
import icons from "@/constants/icons";
|
import icons from "@/constants/icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { cars, featuredCars } from "@/constants/data";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
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 (
|
return (
|
||||||
<TouchableOpacity className={'flex flex-col items-start w-60 h-60 relative'}>
|
<TouchableOpacity
|
||||||
<Image source={images.car1} className={'w-60 h-40 rounded-2xl'} resizeMode={"contain"}/>
|
onPress={() => router.push(`/cars/${data.$id || data.id}`)}
|
||||||
<Image source={images.cardGradient} className={'size-full rounded-2xl absolute bottom-0'}/>
|
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'}>
|
<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 text-base font-bold'} numberOfLines={1}>{data.title}</Text>
|
||||||
<Text className={'text-white'}>Skopje</Text>
|
<Text className={'text-white'}>{data.location}</Text>
|
||||||
|
|
||||||
<View className={'flex flex-row items-center justify-between w-full'}>
|
<View className={'flex flex-row items-center justify-between w-full'}>
|
||||||
<Text className={'text-base text-white'}>
|
<Text className={'text-base text-white'}>€{data.price}</Text>
|
||||||
25.000
|
|
||||||
</Text>
|
|
||||||
<Image source={icons.heart} className={'size-5'}/>
|
<Image source={icons.heart} className={'size-5'}/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -33,18 +57,28 @@ export const FeaturedCard = ({onPress} :Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Card = ({onPress} :Props) => {
|
|
||||||
|
export const Card = ({data} :Props) => {
|
||||||
return (
|
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'}>
|
<TouchableOpacity
|
||||||
<View className={'flex flex-row items-center absolute px-2 top-5 right-5 bg-white/90 p-1 rounded-full z-50'}>
|
onPress={() => router.push(`/cars/${data.$id || data.id}`)}
|
||||||
<Image source={icons.star} className={'size-2.5'}/>
|
className={'flex-1 w-full mt-4 px-3 py-4 rounded-xl bg-white shadow-lg shadow-black-100/70 relative'}
|
||||||
<Text className={'text-sm ml-1'}>5.0</Text>
|
>
|
||||||
</View>
|
<Image
|
||||||
<Image source={images.car2} className={'w-full h-40 rounded-lg'}/>
|
source={{ uri: data.images?.[0] || data.image }}
|
||||||
|
className={'w-full h-40 rounded-lg'}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
<View className={'flex flex-col mt-2'}>
|
<View className={'flex flex-col mt-2'}>
|
||||||
<Text className={'text-base text-gray-600'}>xyz</Text>
|
<Text className={'text-base text-gray-600'}>{data?.title}</Text>
|
||||||
<Text className={'text-sm'}>Skopje</Text>
|
<Text className={'text-sm'}>{data?.location}</Text>
|
||||||
|
<Text className={'text-base text-blue-500'}>€{data?.price}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CarDetails = () => {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const carData = [...cars, ...featuredCars].find(car => car.id === id);
|
||||||
}
|
}
|
||||||
63
components/ImagePicker.tsx
Normal file
63
components/ImagePicker.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,41 +1,21 @@
|
|||||||
import {View, Text, TextInput, SafeAreaView, Image, TouchableOpacity} from 'react-native'
|
import React from 'react';
|
||||||
import React, {useState} from 'react'
|
import { View, TextInput, TouchableOpacity, Image } from 'react-native';
|
||||||
import {router, useLocalSearchParams, usePathname} from "expo-router";
|
import icons from '@/constants/icons';
|
||||||
import icons from "@/constants/icons";
|
|
||||||
import {useDebouncedCallback} from "use-debounce";
|
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const path = usePathname();
|
return (
|
||||||
const params = useLocalSearchParams<{query?: string}>();
|
<View className="flex-row items-center bg-gray-100 rounded-full px-4 py-2">
|
||||||
const [search, setSearch] = useState(params.query);
|
<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
|
export default Search;
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@ -1,162 +1,88 @@
|
|||||||
import icons from "./icons";
|
import icons from "./icons";
|
||||||
import images from "./images";
|
import images from "./images";
|
||||||
|
|
||||||
export const cards = [
|
export const cars = [
|
||||||
{
|
{
|
||||||
title: "Card 1",
|
id: "1",
|
||||||
location: "Location 1",
|
title: "BMW M4 Competition",
|
||||||
price: "$100",
|
location: "Skopje, Macedonia",
|
||||||
|
price: "85,000",
|
||||||
rating: 4.8,
|
rating: 4.8,
|
||||||
category: "house",
|
images: "https://images.unsplash.com/photo-1617814076668-4af3ff8c4a26?q=80&w=800",
|
||||||
image: images.newYork,
|
year: "2023",
|
||||||
|
fuelType: "Petrol",
|
||||||
|
transmission: "Automatic",
|
||||||
|
mileage: "15,000",
|
||||||
|
description: "Pristine condition BMW M4 Competition with full service history.",
|
||||||
|
category: "Sedan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Card 2",
|
id: "2",
|
||||||
location: "Location 2",
|
title: "Mercedes-Benz G63 AMG",
|
||||||
price: "$200",
|
location: "Ohrid, Macedonia",
|
||||||
rating: 3,
|
price: "180,000",
|
||||||
category: "house",
|
rating: 5.0,
|
||||||
image: images.japan,
|
images: "https://images.unsplash.com/photo-1520031441872-265e4ff70366?q=80&w=800",
|
||||||
|
year: "2024",
|
||||||
|
fuelType: "Petrol",
|
||||||
|
transmission: "Automatic",
|
||||||
|
mileage: "5,000",
|
||||||
|
description: "Brand new G63 AMG with all available options.",
|
||||||
|
category: "SUV"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Card 3",
|
id: "3",
|
||||||
location: "Location 3",
|
title: "Porsche 911 GT3",
|
||||||
price: "$300",
|
location: "Bitola, Macedonia",
|
||||||
rating: 2,
|
price: "195,000",
|
||||||
category: "flat",
|
rating: 4.9,
|
||||||
image: images.newYork,
|
images: "https://images.unsplash.com/photo-1614162692292-7ac56d7f7f1e?q=80&w=800",
|
||||||
},
|
year: "2023",
|
||||||
{
|
fuelType: "Petrol",
|
||||||
title: "Card 4",
|
transmission: "Manual",
|
||||||
location: "Location 4",
|
mileage: "1,200",
|
||||||
price: "$400",
|
description: "Limited edition GT3 in Racing Yellow.",
|
||||||
rating: 5,
|
category: "Sports"
|
||||||
category: "villa",
|
}
|
||||||
image: images.japan,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const featuredCards = [
|
export const featuredCars = [
|
||||||
{
|
{
|
||||||
title: "Featured 1",
|
id: "4",
|
||||||
location: "Location 1",
|
title: "Audi RS e-tron GT",
|
||||||
price: "$100",
|
location: "Skopje, Macedonia",
|
||||||
rating: 4.8,
|
price: "145,000",
|
||||||
image: images.newYork,
|
rating: 4.7,
|
||||||
category: "house",
|
image: "https://images.unsplash.com/photo-1614200187524-dc4b892acf16?q=80&w=800",
|
||||||
|
category: "Electric",
|
||||||
|
year: "2024",
|
||||||
|
fuelType: "Electric",
|
||||||
|
transmission: "Automatic",
|
||||||
|
mileage: "1,000",
|
||||||
|
description: "Brand new Audi RS e-tron GT, fully electric performance sedan."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Featured 2",
|
id: "5",
|
||||||
location: "Location 2",
|
title: "Ferrari F8 Tributo",
|
||||||
price: "$200",
|
location: "Tetovo, Macedonia",
|
||||||
rating: 3,
|
price: "335,000",
|
||||||
image: images.japan,
|
rating: 5.0,
|
||||||
category: "flat",
|
image: "https://images.unsplash.com/photo-1592198084033-aade902d1aae?q=80&w=800",
|
||||||
},
|
category: "Sports",
|
||||||
|
year: "2023",
|
||||||
|
fuelType: "Petrol",
|
||||||
|
transmission: "Automatic",
|
||||||
|
mileage: "500",
|
||||||
|
description: "Ferrari F8 Tributo in Rosso Corsa, like new condition."
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const categories = [
|
export const categories = [
|
||||||
{ title: "All", category: "All" },
|
{ title: "All", category: "All" },
|
||||||
{ title: "Sedan", category: "Sedan" },
|
{ title: "Sedan", category: "Sedan" },
|
||||||
{ title: "Cabrio", category: "Cabrio" },
|
{ title: "SUV", category: "SUV" },
|
||||||
{ title: "Van", category: "Van" },
|
{ title: "Sports", category: "Sports" },
|
||||||
{ title: "Motorcicle", category: "Motorcircle" },
|
{ title: "Electric", category: "Electric" },
|
||||||
{ title: "Truck", category: "Truck" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const settings = [
|
|
||||||
{
|
|
||||||
title: "My Bookings",
|
|
||||||
icon: icons.calendar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Payments",
|
|
||||||
icon: icons.wallet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Profile",
|
|
||||||
icon: icons.person,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Notifications",
|
|
||||||
icon: icons.bell,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Security",
|
|
||||||
icon: icons.shield,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Language",
|
|
||||||
icon: icons.language,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Help Center",
|
|
||||||
icon: icons.info,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Invite Friends",
|
|
||||||
icon: icons.people,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const facilities = [
|
|
||||||
{
|
|
||||||
title: "Laundry",
|
|
||||||
icon: icons.laundry,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Car Parking",
|
|
||||||
icon: icons.carPark,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Sports Center",
|
|
||||||
icon: icons.run,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Cutlery",
|
|
||||||
icon: icons.cutlery,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Gym",
|
|
||||||
icon: icons.dumbell,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Swimming pool",
|
|
||||||
icon: icons.swim,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Wifi",
|
|
||||||
icon: icons.wifi,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Pet Center",
|
|
||||||
icon: icons.dog,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const gallery = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
image: images.newYork,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
image: images.japan,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
image: images.newYork,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
image: images.japan,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
image: images.newYork,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
image: images.japan,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import {Account, Avatars, Client, Databases, OAuthProvider, Query} from "react-native-appwrite";
|
import { Account, Avatars, Client, Databases, OAuthProvider, Query } from "react-native-appwrite";
|
||||||
import * as Linking from "expo-linking"
|
import * as Linking from "expo-linking"
|
||||||
import {openAuthSessionAsync} from "expo-web-browser";
|
import { openAuthSessionAsync } from "expo-web-browser";
|
||||||
import {attribute} from "postcss-selector-parser";
|
import { attribute } from "postcss-selector-parser";
|
||||||
import limit from "ajv-formats/src/limit";
|
import limit from "ajv-formats/src/limit";
|
||||||
|
import { Storage, ID } from 'react-native-appwrite';
|
||||||
|
|
||||||
// Add this debug log
|
// Add this debug log
|
||||||
// console.log('Environment Variables:', {
|
// console.log('Environment Variables:', {
|
||||||
@ -20,30 +21,31 @@ export const config = {
|
|||||||
projectId: process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID,
|
projectId: process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID,
|
||||||
databaseId: process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID,
|
databaseId: process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID,
|
||||||
usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID,
|
usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID,
|
||||||
// oglasuvacId: process.env.EXPO_PUBLIC_APPWRITE_OGLASUVAC_COLLECTION_ID,
|
carId: process.env.EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID,
|
||||||
// galleryId: process.env.EXPO_PUBLIC_APPWRITE_GALERIES_COLLECTION_ID,
|
bucketId: process.env.EXPO_PUBLIC_APPWRITE_BUCKET_ID,
|
||||||
// reviewId: process.env.EXPO_PUBLIC_APPWRITE_REVIEWS_COLLECTION_ID,
|
|
||||||
// vehicleId: process.env.EXPO_PUBLIC_APPWRITE_VEHICLE_COLLECTION_ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const client = new Client();
|
export const client = new Client();
|
||||||
|
|
||||||
client
|
client
|
||||||
.setEndpoint(config.endpoint!)
|
.setEndpoint(config.endpoint!)
|
||||||
.setProject(config.projectId!)
|
.setProject(config.projectId!)
|
||||||
.setPlatform(config.platform!)
|
.setPlatform(config.platform!)
|
||||||
|
|
||||||
export const avatar = new Avatars(client);
|
export const avatar = new Avatars(client);
|
||||||
export const account = new Account(client);
|
export const account = new Account(client);
|
||||||
export const databases = new Databases(client);
|
export const databases = new Databases(client);
|
||||||
|
|
||||||
export async function login (){
|
// Initialize storage
|
||||||
|
export const storageClient = new Storage(client);
|
||||||
|
|
||||||
|
export async function login() {
|
||||||
// Your code here to authenticate the user and return their JWT token.
|
// Your code here to authenticate the user and return their JWT token.
|
||||||
try {
|
try {
|
||||||
const redirectUri = Linking.createURL('/');
|
const redirectUri = Linking.createURL('/');
|
||||||
const response = await account.createOAuth2Token(OAuthProvider.Google, redirectUri);
|
const response = await account.createOAuth2Token(OAuthProvider.Google, redirectUri);
|
||||||
|
|
||||||
if(!response) throw new Error('failed to login');
|
if (!response) throw new Error('failed to login');
|
||||||
|
|
||||||
const browserResult = await openAuthSessionAsync(
|
const browserResult = await openAuthSessionAsync(
|
||||||
response.toString(),
|
response.toString(),
|
||||||
@ -59,12 +61,12 @@ export async function login (){
|
|||||||
const secret = url.searchParams.get('secret')?.toString();
|
const secret = url.searchParams.get('secret')?.toString();
|
||||||
const userId = url.searchParams.get('userId')?.toString();
|
const userId = url.searchParams.get('userId')?.toString();
|
||||||
|
|
||||||
if (!secret ||!userId) {
|
if (!secret || !userId) {
|
||||||
throw new Error('failed to authenticate user');
|
throw new Error('failed to authenticate user');
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await account.createSession(userId, secret);
|
const session = await account.createSession(userId, secret);
|
||||||
if (!session) throw new Error('failed to create session');
|
if (!session) throw new Error('failed to create session');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@ -76,7 +78,7 @@ export async function login (){
|
|||||||
export async function getCurrentUser() {
|
export async function getCurrentUser() {
|
||||||
try {
|
try {
|
||||||
const response = await account.get();
|
const response = await account.get();
|
||||||
if(response.$id){
|
if (response.$id) {
|
||||||
const userAvatar = avatar.getInitials(response.name)
|
const userAvatar = avatar.getInitials(response.name)
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
@ -99,30 +101,6 @@ export async function logOut(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export async function getData() {
|
|
||||||
// try {
|
|
||||||
// const result =await databases.listDocuments(
|
|
||||||
// config.databaseId!,
|
|
||||||
// config.vehicleId!,
|
|
||||||
// [Query.orderAsc(`$createdAt`), Query.limit(5)],
|
|
||||||
|
|
||||||
// )
|
|
||||||
// console.log(result.documents)
|
|
||||||
// return result.documents;
|
|
||||||
// }
|
|
||||||
// catch (error) {
|
|
||||||
// console.error("Failed to fetch data:", error);
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export async function getVehicles({filter, query, limit}: {
|
|
||||||
// filter?: string;
|
|
||||||
// query?: string;
|
|
||||||
// limit?: number;
|
|
||||||
// }){
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
export async function saveUserToDatabase(userData: {
|
export async function saveUserToDatabase(userData: {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -161,3 +139,39 @@ export async function saveUserToDatabase(userData: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add image upload function
|
||||||
|
export async function uploadImage(imageFile: {
|
||||||
|
uri: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const response = await storageClient.createFile(
|
||||||
|
config.bucketId!,
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
uri: imageFile.uri,
|
||||||
|
name: imageFile.name || 'image.jpg',
|
||||||
|
type: imageFile.type || 'image/jpeg',
|
||||||
|
size: imageFile.size || 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return storageClient.getFileView(config.bucketId!, response.$id).toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading image:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add image delete function
|
||||||
|
export async function deleteImage(fileId: string) {
|
||||||
|
try {
|
||||||
|
await storageClient.deleteFile(config.bucketId!, fileId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting image:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
121
lib/database.ts
Normal file
121
lib/database.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { databases, client, config } from './appwrite';
|
||||||
|
import { ID, Query } from 'react-native-appwrite'
|
||||||
|
|
||||||
|
// Car type based on your database schema
|
||||||
|
interface Car {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
make: string;
|
||||||
|
model: string;
|
||||||
|
year: number;
|
||||||
|
fuelType: 'Petrol' | 'Diesel' | 'Electric' | 'Hybrid';
|
||||||
|
transmission: 'Manual' | 'Automatic';
|
||||||
|
location: string;
|
||||||
|
postedBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
images: string[];
|
||||||
|
featured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Car functions
|
||||||
|
export async function createCar(carData: Omit<Car, 'id' | 'createdAt'>) {
|
||||||
|
try {
|
||||||
|
return await databases.createDocument(
|
||||||
|
config.databaseId!,
|
||||||
|
config.carId!,
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
...carData,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating car:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCars() {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
config.databaseId!,
|
||||||
|
config.carId!,
|
||||||
|
[Query.orderDesc('$createdAt')]
|
||||||
|
);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cars:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFeaturedCars() {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
config.databaseId!,
|
||||||
|
config.carId!,
|
||||||
|
[
|
||||||
|
Query.orderDesc('$createdAt'),
|
||||||
|
Query.limit(5)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching featured cars:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCar(id: string) {
|
||||||
|
try {
|
||||||
|
const response = await databases.getDocument(
|
||||||
|
config.databaseId!,
|
||||||
|
config.carId!,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching car:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favorites functions
|
||||||
|
export async function toggleFavorite(userId: string, carId: string) {
|
||||||
|
try {
|
||||||
|
const favorite = await databases.listDocuments(
|
||||||
|
config.databaseId!,
|
||||||
|
'favorites',
|
||||||
|
[
|
||||||
|
Query.equal('userId', userId),
|
||||||
|
Query.equal('carId', carId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (favorite.documents.length > 0) {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
config.databaseId!,
|
||||||
|
'favorites',
|
||||||
|
favorite.documents[0].$id
|
||||||
|
);
|
||||||
|
return false; // Unfavorited
|
||||||
|
} else {
|
||||||
|
await databases.createDocument(
|
||||||
|
config.databaseId!,
|
||||||
|
'favorites',
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
carId,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return true; // Favorited
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling favorite:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,10 @@ interface UseAppwriteReturn<T, P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useAppwrite = <T, P extends Record<string, string | number>>({
|
export const useAppwrite = <T, P extends Record<string, string | number>>({
|
||||||
fn,
|
fn,
|
||||||
params = {} as P,
|
params = {} as P,
|
||||||
skip = false,
|
skip = false,
|
||||||
}: UseAppwriteOptions<T, P>): UseAppwriteReturn<T, P> => {
|
}: UseAppwriteOptions<T, P>): UseAppwriteReturn<T, P> => {
|
||||||
const [data, setData] = useState<T | null>(null);
|
const [data, setData] = useState<T | null>(null);
|
||||||
const [loading, setLoading] = useState(!skip);
|
const [loading, setLoading] = useState(!skip);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"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",
|
||||||
@ -16,6 +17,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",
|
||||||
@ -4824,6 +4826,16 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-picker/picker": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-khEhIW/uhfMqq/+tvg4rEAiPGT8GX+Y6QydlP2TSMSmRHoSJK+ShXvXZXSr4Sii4imkj4BwvLunGywwtQDODqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.76.5",
|
"version": "0.76.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz",
|
||||||
@ -8517,6 +8529,27 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-image-loader": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-image-picker": {
|
||||||
|
"version": "16.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.5.tgz",
|
||||||
|
"integrity": "sha512-Rkc4vihqPN/48PV3QJkrG10Lg5s1LonKmpVnGwBSlHjRR5ngqJH++DYFndgD69hLaWrJukzCYpP3CyTMAAatWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-image-loader": "~5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-keep-awake": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "14.0.1",
|
"version": "14.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.1.tgz",
|
||||||
|
|||||||
@ -43,7 +43,9 @@
|
|||||||
"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