├── nativewind-env.d.ts ├── app ├── global.css ├── (root) │ ├── _layout.tsx │ ├── (tabs) │ │ ├── _layout.tsx │ │ ├── explore.tsx │ │ ├── profile.tsx │ │ └── index.tsx │ └── properties │ │ └── [id].tsx ├── _layout.tsx └── sign-in.tsx ├── assets ├── icons │ ├── area.png │ ├── bath.png │ ├── bed.png │ ├── bell.png │ ├── chat.png │ ├── dog.png │ ├── edit.png │ ├── heart.png │ ├── home.png │ ├── info.png │ ├── phone.png │ ├── run.png │ ├── send.png │ ├── star.png │ ├── swim.png │ ├── wifi.png │ ├── cutlery.png │ ├── dumbell.png │ ├── filter.png │ ├── google.png │ ├── laundry.png │ ├── logout.png │ ├── people.png │ ├── person.png │ ├── search.png │ ├── shield.png │ ├── wallet.png │ ├── back-arrow.png │ ├── calendar.png │ ├── car-park.png │ ├── language.png │ ├── location.png │ └── right-arrow.png ├── images │ ├── icon.png │ ├── map.png │ ├── avatar.png │ ├── japan.png │ ├── splash.png │ ├── bar-chart.png │ ├── favicon.png │ ├── new-york.png │ ├── no-result.png │ ├── onboarding.png │ ├── splash-icon.png │ ├── card-gradient.png │ └── white-gradient.png ├── readme │ ├── hero.webp │ ├── jsmpro.webp │ └── videokit.webp └── fonts │ ├── Rubik-Bold.ttf │ ├── Rubik-Light.ttf │ ├── Rubik-Medium.ttf │ ├── Rubik-ExtraBold.ttf │ ├── Rubik-Regular.ttf │ └── Rubik-SemiBold.ttf ├── babel.config.js ├── metro.config.js ├── tsconfig.json ├── .vscode └── settings.json ├── image.d.ts ├── .gitignore ├── components ├── NoResults.tsx ├── Comment.tsx ├── Search.tsx ├── Filters.tsx └── Cards.tsx ├── constants ├── images.ts ├── icons.ts └── data.ts ├── tailwind.config.js ├── lib ├── global-provider.tsx ├── useAppwrite.ts ├── appwrite.ts ├── data.ts └── seed.ts ├── app.json ├── package.json └── README.md /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /assets/icons/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/area.png -------------------------------------------------------------------------------- /assets/icons/bath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/bath.png -------------------------------------------------------------------------------- /assets/icons/bed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/bed.png -------------------------------------------------------------------------------- /assets/icons/bell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/bell.png -------------------------------------------------------------------------------- /assets/icons/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/chat.png -------------------------------------------------------------------------------- /assets/icons/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/dog.png -------------------------------------------------------------------------------- /assets/icons/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/edit.png -------------------------------------------------------------------------------- /assets/icons/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/heart.png -------------------------------------------------------------------------------- /assets/icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/home.png -------------------------------------------------------------------------------- /assets/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/info.png -------------------------------------------------------------------------------- /assets/icons/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/phone.png -------------------------------------------------------------------------------- /assets/icons/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/run.png -------------------------------------------------------------------------------- /assets/icons/send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/send.png -------------------------------------------------------------------------------- /assets/icons/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/star.png -------------------------------------------------------------------------------- /assets/icons/swim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/swim.png -------------------------------------------------------------------------------- /assets/icons/wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/wifi.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/map.png -------------------------------------------------------------------------------- /assets/icons/cutlery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/cutlery.png -------------------------------------------------------------------------------- /assets/icons/dumbell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/dumbell.png -------------------------------------------------------------------------------- /assets/icons/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/filter.png -------------------------------------------------------------------------------- /assets/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/google.png -------------------------------------------------------------------------------- /assets/icons/laundry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/laundry.png -------------------------------------------------------------------------------- /assets/icons/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/logout.png -------------------------------------------------------------------------------- /assets/icons/people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/people.png -------------------------------------------------------------------------------- /assets/icons/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/person.png -------------------------------------------------------------------------------- /assets/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/search.png -------------------------------------------------------------------------------- /assets/icons/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/shield.png -------------------------------------------------------------------------------- /assets/icons/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/wallet.png -------------------------------------------------------------------------------- /assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/avatar.png -------------------------------------------------------------------------------- /assets/images/japan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/japan.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/readme/hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/readme/hero.webp -------------------------------------------------------------------------------- /assets/fonts/Rubik-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-Bold.ttf -------------------------------------------------------------------------------- /assets/icons/back-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/back-arrow.png -------------------------------------------------------------------------------- /assets/icons/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/calendar.png -------------------------------------------------------------------------------- /assets/icons/car-park.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/car-park.png -------------------------------------------------------------------------------- /assets/icons/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/language.png -------------------------------------------------------------------------------- /assets/icons/location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/location.png -------------------------------------------------------------------------------- /assets/images/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/bar-chart.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/new-york.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/new-york.png -------------------------------------------------------------------------------- /assets/images/no-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/no-result.png -------------------------------------------------------------------------------- /assets/readme/jsmpro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/readme/jsmpro.webp -------------------------------------------------------------------------------- /assets/readme/videokit.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/readme/videokit.webp -------------------------------------------------------------------------------- /assets/fonts/Rubik-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Rubik-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-Medium.ttf -------------------------------------------------------------------------------- /assets/icons/right-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/icons/right-arrow.png -------------------------------------------------------------------------------- /assets/images/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/onboarding.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/fonts/Rubik-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Rubik-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Rubik-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/fonts/Rubik-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/card-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/card-gradient.png -------------------------------------------------------------------------------- /assets/images/white-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react_native-restate/HEAD/assets/images/white-gradient.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const { withNativeWind } = require("nativewind/metro"); 3 | 4 | const config = getDefaultConfig(__dirname); 5 | 6 | module.exports = withNativeWind(config, { input: "./app/global.css" }); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts", 16 | "nativewind-env.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true, 4 | "[typescriptreact]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit" 12 | }, 13 | "typescript.tsdk": "node_modules\\typescript\\lib" 14 | } -------------------------------------------------------------------------------- /image.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const value: any; 8 | export default value; 9 | } 10 | 11 | declare module "*.jpeg" { 12 | const value: any; 13 | export default value; 14 | } 15 | 16 | declare module "*.gif" { 17 | const value: any; 18 | export default value; 19 | } 20 | 21 | declare module "*.svg" { 22 | const value: any; 23 | export default value; 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | app-example 39 | -------------------------------------------------------------------------------- /components/NoResults.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, Image } from "react-native"; 3 | 4 | import images from "@/constants/images"; 5 | 6 | const NoResults = () => { 7 | return ( 8 | 9 | 14 | 15 | No Result 16 | 17 | 18 | We could not find any result 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default NoResults; 25 | -------------------------------------------------------------------------------- /constants/images.ts: -------------------------------------------------------------------------------- 1 | import onboarding from "@/assets/images/onboarding.png"; 2 | import avatar from "@/assets/images/avatar.png"; 3 | import newYork from "@/assets/images/new-york.png"; 4 | import japan from "@/assets/images/japan.png"; 5 | import cardGradient from "@/assets/images/card-gradient.png"; 6 | import barChart from "@/assets/images/bar-chart.png"; 7 | import whiteGradient from "@/assets/images/white-gradient.png"; 8 | import map from "@/assets/images/map.png"; 9 | import noResult from "@/assets/images/no-result.png"; 10 | 11 | export default { 12 | onboarding, 13 | avatar, 14 | newYork, 15 | japan, 16 | cardGradient, 17 | barChart, 18 | whiteGradient, 19 | map, 20 | noResult, 21 | }; 22 | -------------------------------------------------------------------------------- /app/(root)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Slot } from "expo-router"; 2 | import { ActivityIndicator } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | 5 | import { useGlobalContext } from "@/lib/global-provider"; 6 | 7 | export default function AppLayout() { 8 | const { loading, isLogged } = useGlobalContext(); 9 | 10 | if (loading) { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | if (!isLogged) { 19 | return ; 20 | } 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], 4 | presets: [require("nativewind/preset")], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | rubik: ["Rubik-Regular", "sans-serif"], 9 | "rubik-bold": ["Rubik-Bold", "sans-serif"], 10 | "rubik-extrabold": ["Rubik-ExtraBold", "sans-serif"], 11 | "rubik-medium": ["Rubik-Medium", "sans-serif"], 12 | "rubik-semibold": ["Rubik-SemiBold", "sans-serif"], 13 | "rubik-light": ["Rubik-Light", "sans-serif"], 14 | }, 15 | colors: { 16 | primary: { 17 | 100: "#0061FF0A", 18 | 200: "#0061FF1A", 19 | 300: "#0061FF", 20 | }, 21 | accent: { 22 | 100: "#FBFBFD", 23 | }, 24 | black: { 25 | DEFAULT: "#000000", 26 | 100: "#8C8E98", 27 | 200: "#666876", 28 | 300: "#191D31", 29 | }, 30 | danger: "#F75555", 31 | }, 32 | }, 33 | }, 34 | plugins: [], 35 | }; 36 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Stack } from "expo-router"; 3 | import { useFonts } from "expo-font"; 4 | import * as SplashScreen from "expo-splash-screen"; 5 | 6 | import "./global.css"; 7 | import GlobalProvider from "@/lib/global-provider"; 8 | 9 | export default function RootLayout() { 10 | const [fontsLoaded] = useFonts({ 11 | "Rubik-Bold": require("../assets/fonts/Rubik-Bold.ttf"), 12 | "Rubik-ExtraBold": require("../assets/fonts/Rubik-ExtraBold.ttf"), 13 | "Rubik-Light": require("../assets/fonts/Rubik-Light.ttf"), 14 | "Rubik-Medium": require("../assets/fonts/Rubik-Medium.ttf"), 15 | "Rubik-Regular": require("../assets/fonts/Rubik-Regular.ttf"), 16 | "Rubik-SemiBold": require("../assets/fonts/Rubik-SemiBold.ttf"), 17 | }); 18 | 19 | useEffect(() => { 20 | if (fontsLoaded) { 21 | SplashScreen.hideAsync(); 22 | } 23 | }, [fontsLoaded]); 24 | 25 | if (!fontsLoaded) { 26 | return null; 27 | } 28 | 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/global-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, ReactNode } from "react"; 2 | 3 | import { getCurrentUser } from "./appwrite"; 4 | import { useAppwrite } from "./useAppwrite"; 5 | import { Redirect } from "expo-router"; 6 | 7 | interface GlobalContextType { 8 | isLogged: boolean; 9 | user: User | null; 10 | loading: boolean; 11 | refetch: () => void; 12 | } 13 | 14 | interface User { 15 | $id: string; 16 | name: string; 17 | email: string; 18 | avatar: string; 19 | } 20 | 21 | const GlobalContext = createContext(undefined); 22 | 23 | interface GlobalProviderProps { 24 | children: ReactNode; 25 | } 26 | 27 | export const GlobalProvider = ({ children }: GlobalProviderProps) => { 28 | const { 29 | data: user, 30 | loading, 31 | refetch, 32 | } = useAppwrite({ 33 | fn: getCurrentUser, 34 | }); 35 | 36 | const isLogged = !!user; 37 | 38 | return ( 39 | 47 | {children} 48 | 49 | ); 50 | }; 51 | 52 | export const useGlobalContext = (): GlobalContextType => { 53 | const context = useContext(GlobalContext); 54 | if (!context) 55 | throw new Error("useGlobalContext must be used within a GlobalProvider"); 56 | 57 | return context; 58 | }; 59 | 60 | export default GlobalProvider; 61 | -------------------------------------------------------------------------------- /components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Image } from "react-native"; 2 | 3 | import images from "@/constants/images"; 4 | import icons from "@/constants/icons"; 5 | import { Models } from "react-native-appwrite"; 6 | 7 | interface Props { 8 | item: Models.Document; 9 | } 10 | 11 | const Comment = ({ item }: Props) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | {item.name} 18 | 19 | 20 | 21 | 22 | {item.review} 23 | 24 | 25 | 26 | 27 | 32 | 33 | 120 34 | 35 | 36 | 37 | {new Date(item.$createdAt).toDateString()} 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Comment; 45 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "restate", 4 | "slug": "restate", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true 13 | }, 14 | "android": { 15 | "adaptiveIcon": { 16 | "foregroundImage": "./assets/images/icon.png", 17 | "backgroundColor": "#ffffff" 18 | } 19 | }, 20 | "web": { 21 | "bundler": "metro", 22 | "output": "static", 23 | "favicon": "./assets/images/favicon.png" 24 | }, 25 | "plugins": [ 26 | "expo-router", 27 | [ 28 | "expo-splash-screen", 29 | { 30 | "image": "./assets/images/splash-icon.png", 31 | "resizeMode": "cover", 32 | "backgroundColor": "#ffffff", 33 | "enableFullScreenImage_legacy": true 34 | } 35 | ], 36 | [ 37 | "expo-font", 38 | { 39 | "fonts": [ 40 | "./assets/fonts/Rubik-Bold.ttf", 41 | "./assets/fonts/Rubik-ExtraBold.ttf", 42 | "./assets/fonts/Rubik-Light.ttf", 43 | "./assets/fonts/Rubik-Medium.ttf", 44 | "./assets/fonts/Rubik-Regular.ttf", 45 | "./assets/fonts/Rubik-SemiBold.ttf" 46 | ] 47 | } 48 | ] 49 | ], 50 | "experiments": { 51 | "typedRoutes": true 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /components/Search.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { View, TouchableOpacity, Image, TextInput } from "react-native"; 3 | import { useDebouncedCallback } from "use-debounce"; 4 | 5 | import icons from "@/constants/icons"; 6 | import { useLocalSearchParams, router, usePathname } from "expo-router"; 7 | 8 | const Search = () => { 9 | const path = usePathname(); 10 | const params = useLocalSearchParams<{ query?: string }>(); 11 | const [search, setSearch] = useState(params.query); 12 | 13 | const debouncedSearch = useDebouncedCallback((text: string) => { 14 | router.setParams({ query: text }); 15 | }, 500); 16 | 17 | const handleSearch = (text: string) => { 18 | setSearch(text); 19 | debouncedSearch(text); 20 | }; 21 | 22 | return ( 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default Search; 42 | -------------------------------------------------------------------------------- /lib/useAppwrite.ts: -------------------------------------------------------------------------------- 1 | import { Alert } from "react-native"; 2 | import { useEffect, useState, useCallback } from "react"; 3 | 4 | interface UseAppwriteOptions> { 5 | fn: (params: P) => Promise; 6 | params?: P; 7 | skip?: boolean; 8 | } 9 | 10 | interface UseAppwriteReturn { 11 | data: T | null; 12 | loading: boolean; 13 | error: string | null; 14 | refetch: (newParams: P) => Promise; 15 | } 16 | 17 | export const useAppwrite = >({ 18 | fn, 19 | params = {} as P, 20 | skip = false, 21 | }: UseAppwriteOptions): UseAppwriteReturn => { 22 | const [data, setData] = useState(null); 23 | const [loading, setLoading] = useState(!skip); 24 | const [error, setError] = useState(null); 25 | 26 | const fetchData = useCallback( 27 | async (fetchParams: P) => { 28 | setLoading(true); 29 | setError(null); 30 | 31 | try { 32 | const result = await fn(fetchParams); 33 | setData(result); 34 | } catch (err: unknown) { 35 | const errorMessage = 36 | err instanceof Error ? err.message : "An unknown error occurred"; 37 | setError(errorMessage); 38 | Alert.alert("Error", errorMessage); 39 | } finally { 40 | setLoading(false); 41 | } 42 | }, 43 | [fn] 44 | ); 45 | 46 | useEffect(() => { 47 | if (!skip) { 48 | fetchData(params); 49 | } 50 | }, []); 51 | 52 | const refetch = async (newParams: P) => await fetchData(newParams); 53 | 54 | return { data, loading, error, refetch }; 55 | }; 56 | -------------------------------------------------------------------------------- /components/Filters.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { router, useLocalSearchParams } from "expo-router"; 3 | import { Text, ScrollView, TouchableOpacity } from "react-native"; 4 | 5 | import { categories } from "@/constants/data"; 6 | 7 | const Filters = () => { 8 | const params = useLocalSearchParams<{ filter?: string }>(); 9 | const [selectedCategory, setSelectedCategory] = useState( 10 | params.filter || "All" 11 | ); 12 | 13 | const handleCategoryPress = (category: string) => { 14 | if (selectedCategory === category) { 15 | setSelectedCategory(""); 16 | router.setParams({ filter: "" }); 17 | return; 18 | } 19 | 20 | setSelectedCategory(category); 21 | router.setParams({ filter: category }); 22 | }; 23 | 24 | return ( 25 | 30 | {categories.map((item, index) => ( 31 | handleCategoryPress(item.category)} 33 | key={index} 34 | className={`flex flex-col items-start mr-4 px-4 py-2 rounded-full ${ 35 | selectedCategory === item.category 36 | ? "bg-primary-300" 37 | : "bg-primary-100 border border-primary-200" 38 | }`} 39 | > 40 | 47 | {item.title} 48 | 49 | 50 | ))} 51 | 52 | ); 53 | }; 54 | 55 | export default Filters; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restate", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@expo/vector-icons": "^14.0.2", 19 | "@react-navigation/bottom-tabs": "^7.0.0", 20 | "@react-navigation/native": "^7.0.0", 21 | "expo": "~52.0.17", 22 | "expo-blur": "~14.0.1", 23 | "expo-constants": "~17.0.3", 24 | "expo-font": "~13.0.1", 25 | "expo-haptics": "~14.0.0", 26 | "expo-linking": "~7.0.3", 27 | "expo-router": "~4.0.11", 28 | "expo-splash-screen": "~0.29.15", 29 | "expo-status-bar": "~2.0.0", 30 | "expo-symbols": "~0.2.0", 31 | "expo-system-ui": "~4.0.5", 32 | "expo-web-browser": "~14.0.1", 33 | "nativewind": "^4.1.23", 34 | "react": "18.3.1", 35 | "react-dom": "18.3.1", 36 | "react-native": "0.76.3", 37 | "react-native-appwrite": "^0.5.0", 38 | "react-native-gesture-handler": "~2.20.2", 39 | "react-native-reanimated": "~3.16.1", 40 | "react-native-safe-area-context": "4.12.0", 41 | "react-native-screens": "~4.1.0", 42 | "react-native-url-polyfill": "^2.0.0", 43 | "react-native-web": "~0.19.13", 44 | "react-native-webview": "13.12.5", 45 | "tailwindcss": "^3.4.16", 46 | "use-debounce": "^10.0.4" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.25.2", 50 | "@types/jest": "^29.5.12", 51 | "@types/react": "~18.3.12", 52 | "@types/react-test-renderer": "^18.3.0", 53 | "jest": "^29.2.1", 54 | "jest-expo": "~52.0.2", 55 | "react-test-renderer": "18.3.1", 56 | "typescript": "^5.3.3" 57 | }, 58 | "private": true 59 | } 60 | -------------------------------------------------------------------------------- /constants/icons.ts: -------------------------------------------------------------------------------- 1 | import google from "@/assets/icons/google.png"; 2 | import home from "@/assets/icons/home.png"; 3 | import search from "@/assets/icons/search.png"; 4 | import person from "@/assets/icons/person.png"; 5 | import bell from "@/assets/icons/bell.png"; 6 | import filter from "@/assets/icons/filter.png"; 7 | import star from "@/assets/icons/star.png"; 8 | import heart from "@/assets/icons/heart.png"; 9 | import backArrow from "@/assets/icons/back-arrow.png"; 10 | import calendar from "@/assets/icons/calendar.png"; 11 | import info from "@/assets/icons/info.png"; 12 | import language from "@/assets/icons/language.png"; 13 | import logout from "@/assets/icons/logout.png"; 14 | import people from "@/assets/icons/people.png"; 15 | import shield from "@/assets/icons/shield.png"; 16 | import wallet from "@/assets/icons/wallet.png"; 17 | import rightArrow from "@/assets/icons/right-arrow.png"; 18 | import send from "@/assets/icons/send.png"; 19 | import bed from "@/assets/icons/bed.png"; 20 | import bath from "@/assets/icons/bath.png"; 21 | import area from "@/assets/icons/area.png"; 22 | import chat from "@/assets/icons/chat.png"; 23 | import phone from "@/assets/icons/phone.png"; 24 | import carPark from "@/assets/icons/car-park.png"; 25 | import cutlery from "@/assets/icons/cutlery.png"; 26 | import dog from "@/assets/icons/dog.png"; 27 | import dumbell from "@/assets/icons/dumbell.png"; 28 | import laundry from "@/assets/icons/laundry.png"; 29 | import run from "@/assets/icons/run.png"; 30 | import swim from "@/assets/icons/swim.png"; 31 | import wifi from "@/assets/icons/wifi.png"; 32 | import location from "@/assets/icons/location.png"; 33 | import edit from "@/assets/icons/edit.png"; 34 | 35 | export default { 36 | google, 37 | home, 38 | search, 39 | person, 40 | bell, 41 | filter, 42 | star, 43 | heart, 44 | backArrow, 45 | calendar, 46 | info, 47 | language, 48 | logout, 49 | people, 50 | shield, 51 | wallet, 52 | rightArrow, 53 | send, 54 | bed, 55 | bath, 56 | area, 57 | chat, 58 | phone, 59 | carPark, 60 | cutlery, 61 | dog, 62 | dumbell, 63 | laundry, 64 | run, 65 | swim, 66 | wifi, 67 | location, 68 | edit, 69 | }; 70 | -------------------------------------------------------------------------------- /app/(root)/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "expo-router"; 2 | import { Image, ImageSourcePropType, Text, View } from "react-native"; 3 | 4 | import icons from "@/constants/icons"; 5 | 6 | const TabIcon = ({ 7 | focused, 8 | icon, 9 | title, 10 | }: { 11 | focused: boolean; 12 | icon: ImageSourcePropType; 13 | title: string; 14 | }) => ( 15 | 16 | 22 | 29 | {title} 30 | 31 | 32 | ); 33 | 34 | const TabsLayout = () => { 35 | return ( 36 | 48 | ( 54 | 55 | ), 56 | }} 57 | /> 58 | ( 64 | 65 | ), 66 | }} 67 | /> 68 | ( 74 | 75 | ), 76 | }} 77 | /> 78 | 79 | ); 80 | }; 81 | 82 | export default TabsLayout; 83 | -------------------------------------------------------------------------------- /app/sign-in.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SafeAreaView } from "react-native-safe-area-context"; 3 | import { 4 | Alert, 5 | Image, 6 | ScrollView, 7 | Text, 8 | TouchableOpacity, 9 | View, 10 | } from "react-native"; 11 | 12 | import { login } from "@/lib/appwrite"; 13 | import { Redirect } from "expo-router"; 14 | import { useGlobalContext } from "@/lib/global-provider"; 15 | import icons from "@/constants/icons"; 16 | import images from "@/constants/images"; 17 | 18 | const Auth = () => { 19 | const { refetch, loading, isLogged } = useGlobalContext(); 20 | 21 | if (!loading && isLogged) return ; 22 | 23 | const handleLogin = async () => { 24 | const result = await login(); 25 | if (result) { 26 | refetch(); 27 | } else { 28 | Alert.alert("Error", "Failed to login"); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 39 | 44 | 45 | 46 | 47 | Welcome To Real Scout 48 | 49 | 50 | 51 | Let's Get You Closer To {"\n"} 52 | Your Ideal Home 53 | 54 | 55 | 56 | Login to Real Scout with Google 57 | 58 | 59 | 63 | 64 | 69 | 70 | Continue with Google 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | }; 79 | 80 | export default Auth; 81 | -------------------------------------------------------------------------------- /app/(root)/(tabs)/explore.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | FlatList, 4 | Image, 5 | SafeAreaView, 6 | Text, 7 | TouchableOpacity, 8 | View, 9 | } from "react-native"; 10 | import { useEffect } from "react"; 11 | import { router, useLocalSearchParams } from "expo-router"; 12 | 13 | import icons from "@/constants/icons"; 14 | import Search from "@/components/Search"; 15 | import { Card } from "@/components/Cards"; 16 | import Filters from "@/components/Filters"; 17 | import NoResults from "@/components/NoResults"; 18 | 19 | import { getProperties } from "@/lib/appwrite"; 20 | import { useAppwrite } from "@/lib/useAppwrite"; 21 | 22 | const Explore = () => { 23 | const params = useLocalSearchParams<{ query?: string; filter?: string }>(); 24 | 25 | const { 26 | data: properties, 27 | refetch, 28 | loading, 29 | } = useAppwrite({ 30 | fn: getProperties, 31 | params: { 32 | filter: params.filter!, 33 | query: params.query!, 34 | }, 35 | skip: true, 36 | }); 37 | 38 | useEffect(() => { 39 | refetch({ 40 | filter: params.filter!, 41 | query: params.query!, 42 | }); 43 | }, [params.filter, params.query]); 44 | 45 | const handleCardPress = (id: string) => router.push(`/properties/${id}`); 46 | 47 | return ( 48 | 49 | ( 53 | handleCardPress(item.$id)} /> 54 | )} 55 | keyExtractor={(item) => item.$id} 56 | contentContainerClassName="pb-32" 57 | columnWrapperClassName="flex gap-5 px-5" 58 | showsVerticalScrollIndicator={false} 59 | ListEmptyComponent={ 60 | loading ? ( 61 | 62 | ) : ( 63 | 64 | ) 65 | } 66 | ListHeaderComponent={() => ( 67 | 68 | 69 | router.back()} 71 | className="flex flex-row bg-primary-200 rounded-full size-11 items-center justify-center" 72 | > 73 | 74 | 75 | 76 | 77 | Search for Your Ideal Home 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Found {properties?.length} Properties 89 | 90 | 91 | 92 | )} 93 | /> 94 | 95 | ); 96 | }; 97 | 98 | export default Explore; 99 | -------------------------------------------------------------------------------- /components/Cards.tsx: -------------------------------------------------------------------------------- 1 | import icons from "@/constants/icons"; 2 | import images from "@/constants/images"; 3 | import { Image, Text, TouchableOpacity, View } from "react-native"; 4 | import { Models } from "react-native-appwrite"; 5 | 6 | interface Props { 7 | item: Models.Document; 8 | onPress?: () => void; 9 | } 10 | 11 | export const FeaturedCard = ({ item, onPress }: Props) => { 12 | return ( 13 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | {item.rating} 28 | 29 | 30 | 31 | 32 | 36 | {item.name} 37 | 38 | 39 | {item.address} 40 | 41 | 42 | 43 | 44 | ${item.price} 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export const Card = ({ item, onPress }: Props) => { 54 | return ( 55 | 59 | 60 | 61 | 62 | {item.rating} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {item.name} 71 | 72 | 73 | {item.address} 74 | 75 | 76 | 77 | 78 | ${item.price} 79 | 80 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /constants/data.ts: -------------------------------------------------------------------------------- 1 | import icons from "./icons"; 2 | import images from "./images"; 3 | 4 | export const cards = [ 5 | { 6 | title: "Card 1", 7 | location: "Location 1", 8 | price: "$100", 9 | rating: 4.8, 10 | category: "house", 11 | image: images.newYork, 12 | }, 13 | { 14 | title: "Card 2", 15 | location: "Location 2", 16 | price: "$200", 17 | rating: 3, 18 | category: "house", 19 | image: images.japan, 20 | }, 21 | { 22 | title: "Card 3", 23 | location: "Location 3", 24 | price: "$300", 25 | rating: 2, 26 | category: "flat", 27 | image: images.newYork, 28 | }, 29 | { 30 | title: "Card 4", 31 | location: "Location 4", 32 | price: "$400", 33 | rating: 5, 34 | category: "villa", 35 | image: images.japan, 36 | }, 37 | ]; 38 | 39 | export const featuredCards = [ 40 | { 41 | title: "Featured 1", 42 | location: "Location 1", 43 | price: "$100", 44 | rating: 4.8, 45 | image: images.newYork, 46 | category: "house", 47 | }, 48 | { 49 | title: "Featured 2", 50 | location: "Location 2", 51 | price: "$200", 52 | rating: 3, 53 | image: images.japan, 54 | category: "flat", 55 | }, 56 | ]; 57 | 58 | export const categories = [ 59 | { title: "All", category: "All" }, 60 | { title: "Houses", category: "House" }, 61 | { title: "Condos", category: "Condos" }, 62 | { title: "Duplexes", category: "Duplexes" }, 63 | { title: "Studios", category: "Studios" }, 64 | { title: "Villas", category: "Villa" }, 65 | { title: "Apartments", category: "Apartments" }, 66 | { title: "Townhomes", category: "Townhomes" }, 67 | { title: "Others", category: "Others" }, 68 | ]; 69 | 70 | export const settings = [ 71 | { 72 | title: "My Bookings", 73 | icon: icons.calendar, 74 | }, 75 | { 76 | title: "Payments", 77 | icon: icons.wallet, 78 | }, 79 | { 80 | title: "Profile", 81 | icon: icons.person, 82 | }, 83 | { 84 | title: "Notifications", 85 | icon: icons.bell, 86 | }, 87 | { 88 | title: "Security", 89 | icon: icons.shield, 90 | }, 91 | { 92 | title: "Language", 93 | icon: icons.language, 94 | }, 95 | { 96 | title: "Help Center", 97 | icon: icons.info, 98 | }, 99 | { 100 | title: "Invite Friends", 101 | icon: icons.people, 102 | }, 103 | ]; 104 | 105 | export const facilities = [ 106 | { 107 | title: "Laundry", 108 | icon: icons.laundry, 109 | }, 110 | { 111 | title: "Car Parking", 112 | icon: icons.carPark, 113 | }, 114 | { 115 | title: "Sports Center", 116 | icon: icons.run, 117 | }, 118 | { 119 | title: "Cutlery", 120 | icon: icons.cutlery, 121 | }, 122 | { 123 | title: "Gym", 124 | icon: icons.dumbell, 125 | }, 126 | { 127 | title: "Swimming pool", 128 | icon: icons.swim, 129 | }, 130 | { 131 | title: "Wifi", 132 | icon: icons.wifi, 133 | }, 134 | { 135 | title: "Pet Center", 136 | icon: icons.dog, 137 | }, 138 | ]; 139 | 140 | export const gallery = [ 141 | { 142 | id: 1, 143 | image: images.newYork, 144 | }, 145 | { 146 | id: 2, 147 | image: images.japan, 148 | }, 149 | { 150 | id: 3, 151 | image: images.newYork, 152 | }, 153 | { 154 | id: 4, 155 | image: images.japan, 156 | }, 157 | { 158 | id: 5, 159 | image: images.newYork, 160 | }, 161 | { 162 | id: 6, 163 | image: images.japan, 164 | }, 165 | ]; 166 | -------------------------------------------------------------------------------- /app/(root)/(tabs)/profile.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Image, 4 | ImageSourcePropType, 5 | SafeAreaView, 6 | ScrollView, 7 | Text, 8 | TouchableOpacity, 9 | View, 10 | } from "react-native"; 11 | 12 | import { logout } from "@/lib/appwrite"; 13 | import { useGlobalContext } from "@/lib/global-provider"; 14 | 15 | import icons from "@/constants/icons"; 16 | import { settings } from "@/constants/data"; 17 | 18 | interface SettingsItemProp { 19 | icon: ImageSourcePropType; 20 | title: string; 21 | onPress?: () => void; 22 | textStyle?: string; 23 | showArrow?: boolean; 24 | } 25 | 26 | const SettingsItem = ({ 27 | icon, 28 | title, 29 | onPress, 30 | textStyle, 31 | showArrow = true, 32 | }: SettingsItemProp) => ( 33 | 37 | 38 | 39 | 40 | {title} 41 | 42 | 43 | 44 | {showArrow && } 45 | 46 | ); 47 | 48 | const Profile = () => { 49 | const { user, refetch } = useGlobalContext(); 50 | 51 | const handleLogout = async () => { 52 | const result = await logout(); 53 | if (result) { 54 | Alert.alert("Success", "Logged out successfully"); 55 | refetch(); 56 | } else { 57 | Alert.alert("Error", "Failed to logout"); 58 | } 59 | }; 60 | 61 | return ( 62 | 63 | 67 | 68 | Profile 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | {user?.name} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {settings.slice(2).map((item, index) => ( 93 | 94 | ))} 95 | 96 | 97 | 98 | 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | export default Profile; 112 | -------------------------------------------------------------------------------- /lib/appwrite.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Client, 3 | Account, 4 | ID, 5 | Databases, 6 | OAuthProvider, 7 | Avatars, 8 | Query, 9 | Storage, 10 | } from "react-native-appwrite"; 11 | import * as Linking from "expo-linking"; 12 | import { openAuthSessionAsync } from "expo-web-browser"; 13 | 14 | export const config = { 15 | platform: "com.jsm.restate", 16 | endpoint: process.env.EXPO_PUBLIC_APPWRITE_ENDPOINT, 17 | projectId: process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID, 18 | databaseId: process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID, 19 | galleriesCollectionId: 20 | process.env.EXPO_PUBLIC_APPWRITE_GALLERIES_COLLECTION_ID, 21 | reviewsCollectionId: process.env.EXPO_PUBLIC_APPWRITE_REVIEWS_COLLECTION_ID, 22 | agentsCollectionId: process.env.EXPO_PUBLIC_APPWRITE_AGENTS_COLLECTION_ID, 23 | propertiesCollectionId: 24 | process.env.EXPO_PUBLIC_APPWRITE_PROPERTIES_COLLECTION_ID, 25 | bucketId: process.env.EXPO_PUBLIC_APPWRITE_BUCKET_ID, 26 | }; 27 | 28 | export const client = new Client(); 29 | client 30 | .setEndpoint(config.endpoint!) 31 | .setProject(config.projectId!) 32 | .setPlatform(config.platform!); 33 | 34 | export const avatar = new Avatars(client); 35 | export const account = new Account(client); 36 | export const databases = new Databases(client); 37 | export const storage = new Storage(client); 38 | 39 | export async function login() { 40 | try { 41 | const redirectUri = Linking.createURL("/"); 42 | 43 | const response = await account.createOAuth2Token( 44 | OAuthProvider.Google, 45 | redirectUri 46 | ); 47 | if (!response) throw new Error("Create OAuth2 token failed"); 48 | 49 | const browserResult = await openAuthSessionAsync( 50 | response.toString(), 51 | redirectUri 52 | ); 53 | if (browserResult.type !== "success") 54 | throw new Error("Create OAuth2 token failed"); 55 | 56 | const url = new URL(browserResult.url); 57 | const secret = url.searchParams.get("secret")?.toString(); 58 | const userId = url.searchParams.get("userId")?.toString(); 59 | if (!secret || !userId) throw new Error("Create OAuth2 token failed"); 60 | 61 | const session = await account.createSession(userId, secret); 62 | if (!session) throw new Error("Failed to create session"); 63 | 64 | return true; 65 | } catch (error) { 66 | console.error(error); 67 | return false; 68 | } 69 | } 70 | 71 | export async function logout() { 72 | try { 73 | const result = await account.deleteSession("current"); 74 | return result; 75 | } catch (error) { 76 | console.error(error); 77 | return false; 78 | } 79 | } 80 | 81 | export async function getCurrentUser() { 82 | try { 83 | const result = await account.get(); 84 | if (result.$id) { 85 | const userAvatar = avatar.getInitials(result.name); 86 | 87 | return { 88 | ...result, 89 | avatar: userAvatar.toString(), 90 | }; 91 | } 92 | 93 | return null; 94 | } catch (error) { 95 | console.log(error); 96 | return null; 97 | } 98 | } 99 | 100 | export async function getLatestProperties() { 101 | try { 102 | const result = await databases.listDocuments( 103 | config.databaseId!, 104 | config.propertiesCollectionId!, 105 | [Query.orderAsc("$createdAt"), Query.limit(5)] 106 | ); 107 | 108 | return result.documents; 109 | } catch (error) { 110 | console.error(error); 111 | return []; 112 | } 113 | } 114 | 115 | export async function getProperties({ 116 | filter, 117 | query, 118 | limit, 119 | }: { 120 | filter: string; 121 | query: string; 122 | limit?: number; 123 | }) { 124 | try { 125 | const buildQuery = [Query.orderDesc("$createdAt")]; 126 | 127 | if (filter && filter !== "All") 128 | buildQuery.push(Query.equal("type", filter)); 129 | 130 | if (query) 131 | buildQuery.push( 132 | Query.or([ 133 | Query.search("name", query), 134 | Query.search("address", query), 135 | Query.search("type", query), 136 | ]) 137 | ); 138 | 139 | if (limit) buildQuery.push(Query.limit(limit)); 140 | 141 | const result = await databases.listDocuments( 142 | config.databaseId!, 143 | config.propertiesCollectionId!, 144 | buildQuery 145 | ); 146 | 147 | return result.documents; 148 | } catch (error) { 149 | console.error(error); 150 | return []; 151 | } 152 | } 153 | 154 | // write function to get property by id 155 | export async function getPropertyById({ id }: { id: string }) { 156 | try { 157 | const result = await databases.getDocument( 158 | config.databaseId!, 159 | config.propertiesCollectionId!, 160 | id 161 | ); 162 | return result; 163 | } catch (error) { 164 | console.error(error); 165 | return null; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/(root)/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | FlatList, 4 | Image, 5 | Text, 6 | TouchableOpacity, 7 | View, 8 | } from "react-native"; 9 | import { useEffect } from "react"; 10 | import { router, useLocalSearchParams } from "expo-router"; 11 | import { SafeAreaView } from "react-native-safe-area-context"; 12 | 13 | import icons from "@/constants/icons"; 14 | 15 | import Search from "@/components/Search"; 16 | import Filters from "@/components/Filters"; 17 | import NoResults from "@/components/NoResults"; 18 | import { Card, FeaturedCard } from "@/components/Cards"; 19 | 20 | import { useAppwrite } from "@/lib/useAppwrite"; 21 | import { useGlobalContext } from "@/lib/global-provider"; 22 | import { getLatestProperties, getProperties } from "@/lib/appwrite"; 23 | 24 | const Home = () => { 25 | const { user } = useGlobalContext(); 26 | 27 | const params = useLocalSearchParams<{ query?: string; filter?: string }>(); 28 | 29 | const { data: latestProperties, loading: latestPropertiesLoading } = 30 | useAppwrite({ 31 | fn: getLatestProperties, 32 | }); 33 | 34 | const { 35 | data: properties, 36 | refetch, 37 | loading, 38 | } = useAppwrite({ 39 | fn: getProperties, 40 | params: { 41 | filter: params.filter!, 42 | query: params.query!, 43 | limit: 6, 44 | }, 45 | skip: true, 46 | }); 47 | 48 | useEffect(() => { 49 | refetch({ 50 | filter: params.filter!, 51 | query: params.query!, 52 | limit: 6, 53 | }); 54 | }, [params.filter, params.query]); 55 | 56 | const handleCardPress = (id: string) => router.push(`/properties/${id}`); 57 | 58 | return ( 59 | 60 | ( 64 | handleCardPress(item.$id)} /> 65 | )} 66 | keyExtractor={(item) => item.$id} 67 | contentContainerClassName="pb-32" 68 | columnWrapperClassName="flex gap-5 px-5" 69 | showsVerticalScrollIndicator={false} 70 | ListEmptyComponent={ 71 | loading ? ( 72 | 73 | ) : ( 74 | 75 | ) 76 | } 77 | ListHeaderComponent={() => ( 78 | 79 | 80 | 81 | 85 | 86 | 87 | 88 | Good Morning 89 | 90 | 91 | {user?.name} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Featured 104 | 105 | 106 | 107 | See all 108 | 109 | 110 | 111 | 112 | {latestPropertiesLoading ? ( 113 | 114 | ) : !latestProperties || latestProperties.length === 0 ? ( 115 | 116 | ) : ( 117 | ( 120 | handleCardPress(item.$id)} 123 | /> 124 | )} 125 | keyExtractor={(item) => item.$id} 126 | horizontal 127 | showsHorizontalScrollIndicator={false} 128 | contentContainerClassName="flex gap-5 mt-5" 129 | /> 130 | )} 131 | 132 | 133 | {/*