diff --git a/.env b/.env index f06a57a..b21e8b6 100644 --- a/.env +++ b/.env @@ -1,11 +1,6 @@ EXPO_PUBLIC_APPWRITE_PROJECT_ID=mobilemk EXPO_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 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_CARS_COLLECTION_ID=67aa22430023baf8dfcb +EXPO_PUBLIC_APPWRITE_BUCKET_ID=67aa2598002da47bee85 diff --git a/app/(root)/(tabs)/index.tsx b/app/(root)/(tabs)/index.tsx index 766d1fd..3eaaf0f 100644 --- a/app/(root)/(tabs)/index.tsx +++ b/app/(root)/(tabs)/index.tsx @@ -7,87 +7,109 @@ import { TouchableOpacity, View, } from "react-native"; -import images from "@/constants/images"; +// import images from "@/constants/images"; import icons from "@/constants/icons"; import Search from "@/components/Search"; import { useGlobalContext } from "@/lib/globalProvider"; import { Card, FeaturedCard } from "@/components/Cards"; 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() { - // create func that determines the time od day [morning, evening, night] const { user, refetch } = useGlobalContext(); + const [cars, setCars] = useState([]); + const [featuredCars, setFeaturedCars] = useState([]); + + 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 ( } - keyExtractor={(item) => item.toString()} + data={cars} + renderItem={({ item }) => } + keyExtractor={(item) => item.$id} numColumns={2} - contentContainerClassName={"pb-32"} - columnWrapperClassName={"flex flex-row gap-5 px-5"} - showsVerticalScrollIndicator={false} ListHeaderComponent={ - - - - - - good morning - {user?.name} - + + {/* User Info */} + + + Welcome back, + {user?.name} - + + + - + {/* Search */} + + + + + {/* Featured Section */} - - featured - + featured - - see all - + see all } - keyExtractor={(item) => item.toString()} + data={featuredCars} + renderItem={({ item }) => } + keyExtractor={(item) => item.$id} horizontal={true} bounces={false} showsHorizontalScrollIndicator={false} contentContainerClassName={"flex gap-3 mt-5"} /> - {/**/} - {/* */} - {/* */} - {/**/} - + + {/* Latest Cars Section */} - - latest cars - + latest cars see all - - - - } + contentContainerClassName={"pb-32"} + columnWrapperClassName={"flex flex-row gap-5 px-5"} + showsVerticalScrollIndicator={false} /> + {/* Add Car Button */} + router.push('/cars/new')} + className="absolute bottom-8 right-8 bg-blue-500 rounded-full p-4 shadow-lg" + > + + + + ); } diff --git a/app/(root)/cars/[id].tsx b/app/(root)/cars/[id].tsx index 933eb57..f3c2e8f 100644 --- a/app/(root)/cars/[id].tsx +++ b/app/(root)/cars/[id].tsx @@ -1,14 +1,118 @@ -import { View, Text } from "react-native"; -import React from "react"; -import { useLocalSearchParams } from "expo-router"; +import { View, Text, SafeAreaView, ScrollView, Image, TouchableOpacity } from "react-native"; +import React, { useEffect, useState } from "react"; +import { useLocalSearchParams, router } from "expo-router"; +import icons from "@/constants/icons"; +import { getCar } from "@/lib/database"; -const Car = () => { +const CarDetails = () => { const { id } = useLocalSearchParams(); + const [carData, setCarData] = useState(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 ( - - Car {id} - + + + {/* Header */} + + + + {/* Back Button */} + router.back()} + className="absolute top-5 left-5 bg-white/90 p-2 rounded-full" + > + + + + {/* Favorite Button */} + + + + + + {/* Content */} + + {/* Title and Rating */} + + {carData.title} + + + {carData.rating} + + + + {/* Location */} + + + {carData.location} + + + {/* Price */} + €{carData.price} + + {/* Details */} + + Car Details + + + {carData.year} + + + {carData.fuelType} + + + {carData.transmission} + + + {carData.mileage} km + + + + + {/* Description */} + + Description + + {carData.description} + + + + + + {/* Bottom Actions */} + + + Message + + + Call + + + ); }; -export default Car; +export default CarDetails; diff --git a/app/(root)/cars/new.tsx b/app/(root)/cars/new.tsx new file mode 100644 index 0000000..0a69e21 --- /dev/null +++ b/app/(root)/cars/new.tsx @@ -0,0 +1,10 @@ +import { SafeAreaView } from 'react-native'; +import { CarForm } from '@/components/CarForm'; + +export default function NewCar() { + return ( + + + + ); +} \ No newline at end of file diff --git a/components/CarForm.tsx b/components/CarForm.tsx new file mode 100644 index 0000000..25a0c47 --- /dev/null +++ b/components/CarForm.tsx @@ -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([]); + 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 ( + + Add New Car + + {/* Images */} + + setImages([...images, url])} + multiple + /> + + {images.length} images selected + + + + {/* Basic Info */} + setFormData({...formData, title: text})} + /> + + setFormData({...formData, description: text})} + /> + + setFormData({...formData, price: text})} + /> + + {/* Car Details */} + + setFormData({...formData, make: text})} + /> + setFormData({...formData, model: text})} + /> + + + + setFormData({...formData, year: text})} + /> + setFormData({...formData, location: text})} + /> + + + {/* Fuel Type Picker */} + + setFormData({...formData, fuelType: value})} + > + {FUEL_TYPES.map((type) => ( + + ))} + + + + {/* Transmission Picker */} + + setFormData({...formData, transmission: value})} + > + {TRANSMISSIONS.map((type) => ( + + ))} + + + + + Add Car + + + ); +}; \ No newline at end of file diff --git a/components/Cards.tsx b/components/Cards.tsx index fe15694..c512003 100644 --- a/components/Cards.tsx +++ b/components/Cards.tsx @@ -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 ( - - - + router.push(`/cars/${data.$id || data.id}`)} + className={'flex flex-col items-start w-60 h-60 relative'} + > + + - - - 5 - - Honda - xyz - Skopje + {data.title} + {data.location} - - 25.000 - + €{data.price} @@ -33,18 +57,28 @@ export const FeaturedCard = ({onPress} :Props) => { ) } -export const Card = ({onPress} :Props) => { + +export const Card = ({data} :Props) => { return ( - - - - 5.0 - - + 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'} + > + - xyz - Skopje + {data?.title} + {data?.location} + €{data?.price} ) +} + +const CarDetails = () => { + const { id } = useLocalSearchParams(); + const carData = [...cars, ...featuredCars].find(car => car.id === id); } \ No newline at end of file diff --git a/components/ImagePicker.tsx b/components/ImagePicker.tsx new file mode 100644 index 0000000..bf83b26 --- /dev/null +++ b/components/ImagePicker.tsx @@ -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 ( + + + {/* change dumbell to camera */} + Upload Image + + ); +}; \ No newline at end of file diff --git a/components/Search.tsx b/components/Search.tsx index 3926eb6..a4932ea 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -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 ( + + + + + + + + ); +}; - // 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 ( - - - - - - - - - - ) -} -export default Search +export default Search; diff --git a/constants/data.ts b/constants/data.ts index 6c587c8..aab86c9 100644 --- a/constants/data.ts +++ b/constants/data.ts @@ -1,162 +1,88 @@ import icons from "./icons"; import images from "./images"; -export const cards = [ +export const cars = [ { - title: "Card 1", - location: "Location 1", - price: "$100", + id: "1", + title: "BMW M4 Competition", + location: "Skopje, Macedonia", + price: "85,000", rating: 4.8, - category: "house", - image: images.newYork, + images: "https://images.unsplash.com/photo-1617814076668-4af3ff8c4a26?q=80&w=800", + year: "2023", + fuelType: "Petrol", + transmission: "Automatic", + mileage: "15,000", + description: "Pristine condition BMW M4 Competition with full service history.", + category: "Sedan" }, { - title: "Card 2", - location: "Location 2", - price: "$200", - rating: 3, - category: "house", - image: images.japan, + id: "2", + title: "Mercedes-Benz G63 AMG", + location: "Ohrid, Macedonia", + price: "180,000", + rating: 5.0, + 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", - location: "Location 3", - price: "$300", - rating: 2, - category: "flat", - image: images.newYork, - }, - { - title: "Card 4", - location: "Location 4", - price: "$400", - rating: 5, - category: "villa", - image: images.japan, - }, + id: "3", + title: "Porsche 911 GT3", + location: "Bitola, Macedonia", + price: "195,000", + rating: 4.9, + images: "https://images.unsplash.com/photo-1614162692292-7ac56d7f7f1e?q=80&w=800", + year: "2023", + fuelType: "Petrol", + transmission: "Manual", + mileage: "1,200", + description: "Limited edition GT3 in Racing Yellow.", + category: "Sports" + } ]; -export const featuredCards = [ +export const featuredCars = [ { - title: "Featured 1", - location: "Location 1", - price: "$100", - rating: 4.8, - image: images.newYork, - category: "house", + id: "4", + title: "Audi RS e-tron GT", + location: "Skopje, Macedonia", + price: "145,000", + rating: 4.7, + 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", - location: "Location 2", - price: "$200", - rating: 3, - image: images.japan, - category: "flat", - }, + id: "5", + title: "Ferrari F8 Tributo", + location: "Tetovo, Macedonia", + price: "335,000", + rating: 5.0, + 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 = [ { title: "All", category: "All" }, { title: "Sedan", category: "Sedan" }, - { title: "Cabrio", category: "Cabrio" }, - { title: "Van", category: "Van" }, - { title: "Motorcicle", category: "Motorcircle" }, - { title: "Truck", category: "Truck" }, + { title: "SUV", category: "SUV" }, + { title: "Sports", category: "Sports" }, + { title: "Electric", category: "Electric" }, ]; -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, - }, -]; diff --git a/lib/appwrite.ts b/lib/appwrite.ts index 0d8b57d..e1244bf 100644 --- a/lib/appwrite.ts +++ b/lib/appwrite.ts @@ -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 {openAuthSessionAsync} from "expo-web-browser"; -import {attribute} from "postcss-selector-parser"; +import { openAuthSessionAsync } from "expo-web-browser"; +import { attribute } from "postcss-selector-parser"; import limit from "ajv-formats/src/limit"; +import { Storage, ID } from 'react-native-appwrite'; // Add this debug log // console.log('Environment Variables:', { @@ -20,30 +21,31 @@ export const config = { projectId: process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID, databaseId: process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID, usersId: process.env.EXPO_PUBLIC_APPWRITE_USERS_COLLECTION_ID, - // oglasuvacId: process.env.EXPO_PUBLIC_APPWRITE_OGLASUVAC_COLLECTION_ID, - // galleryId: process.env.EXPO_PUBLIC_APPWRITE_GALERIES_COLLECTION_ID, - // reviewId: process.env.EXPO_PUBLIC_APPWRITE_REVIEWS_COLLECTION_ID, - // vehicleId: process.env.EXPO_PUBLIC_APPWRITE_VEHICLE_COLLECTION_ID, + carId: process.env.EXPO_PUBLIC_APPWRITE_CARS_COLLECTION_ID, + bucketId: process.env.EXPO_PUBLIC_APPWRITE_BUCKET_ID, } export const client = new Client(); client - .setEndpoint(config.endpoint!) - .setProject(config.projectId!) + .setEndpoint(config.endpoint!) + .setProject(config.projectId!) .setPlatform(config.platform!) export const avatar = new Avatars(client); export const account = new Account(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. try { const redirectUri = Linking.createURL('/'); 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( response.toString(), @@ -59,12 +61,12 @@ export async function login (){ const secret = url.searchParams.get('secret')?.toString(); const userId = url.searchParams.get('userId')?.toString(); - if (!secret ||!userId) { + if (!secret || !userId) { throw new Error('failed to authenticate user'); } 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; } catch (error) { @@ -76,7 +78,7 @@ export async function login (){ export async function getCurrentUser() { try { const response = await account.get(); - if(response.$id){ + if (response.$id) { const userAvatar = avatar.getInitials(response.name) return { ...response, @@ -99,30 +101,6 @@ export async function logOut(): Promise { } } -// 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: { userId: string; @@ -161,3 +139,39 @@ export async function saveUserToDatabase(userData: { 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; + } +} diff --git a/lib/database.ts b/lib/database.ts new file mode 100644 index 0000000..0262eea --- /dev/null +++ b/lib/database.ts @@ -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) { + 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; + } +} \ No newline at end of file diff --git a/lib/useAppwrite.ts b/lib/useAppwrite.ts index def953c..ea3a253 100644 --- a/lib/useAppwrite.ts +++ b/lib/useAppwrite.ts @@ -15,10 +15,10 @@ interface UseAppwriteReturn { } export const useAppwrite = >({ - fn, - params = {} as P, - skip = false, - }: UseAppwriteOptions): UseAppwriteReturn => { + fn, + params = {} as P, + skip = false, +}: UseAppwriteOptions): UseAppwriteReturn => { const [data, setData] = useState(null); const [loading, setLoading] = useState(!skip); const [error, setError] = useState(null); diff --git a/package-lock.json b/package-lock.json index 076e4b8..372f6f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "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", @@ -16,6 +17,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", @@ -4824,6 +4826,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "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": { "version": "0.76.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", @@ -8517,6 +8529,27 @@ "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": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.1.tgz", diff --git a/package.json b/package.json index fbe9d37..38f6354 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "react-native-web": "~0.19.13", "react-native-webview": "13.12.5", "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": { "@babel/core": "^7.25.2",