solomon mulatu
08/12/2025, 4:49 AMTypesenseInstantSearchAdapter
setup in React Native fail to connect, even though raw Axios calls to the same Railway-hosted Typesense endpoint succeed? I'm passing the correct hostname and API key, using port 8080 and protocol 'https'. Could it be an adapter limitation in mobile environments, or is there a mismatch in how the host or connection settings are parsed?
import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
import config from '@/utils/config';
const createTypesenseAdapter = (lat: number, lng: number, radius = '30km') => {
console.log("lat is" + lat + " lng is" + lng + " radius is" + radius);
console.log("config is" + JSON.stringify(config, null, 2));
return new TypesenseInstantSearchAdapter({
server: {
apiKey: apikey,
nodes: [
{
host: 'typesense-production-uytfg.up.railway.app',
port: 8080,
protocol: 'https'
}
],
timeoutSeconds: 50
}
,
additionalSearchParameters: {
query_by: [
'name_en',
'name_am',
'description_en',
'description_am',
'address',
'services.name_en',
'services.name_am',
'services.description_en',
'services.description_am',
],
// Dynamically add location filter
filter_by: lat && lng ? location:([${lat}, ${lng}], radius: ${radius})
: '',
sort_by: lat && lng ? location(${lat}, ${lng}):asc
: 'name_en:asc',
},
})
};
export default createTypesenseAdapter;Kishore Nallan
08/12/2025, 4:49 AMFanis Tharropoulos
08/12/2025, 7:46 AMsolomon mulatu
08/12/2025, 7:47 AMFanis Tharropoulos
08/12/2025, 7:49 AMsolomon mulatu
08/12/2025, 7:49 AMFanis Tharropoulos
08/12/2025, 7:50 AMsolomon mulatu
08/12/2025, 7:54 AMconst axios = require('axios');
let config = {
method: 'get',
maxBodyLength: Infinity,
url: 'https://typesense-production-dfdf.up.railway.app/collections/churches/documents/search?q=geb&query_by=name_en',
headers: {
'X-TYPESENSE-API-KEY': 'apikey'
}
};
axios.request(config)
.then((response) => {
console.log(JSON.stringify(response.data));
})
.catch((error) => {
console.log(error);
});solomon mulatu
08/12/2025, 7:55 AM{
"facet_counts": [],
"found": 0,
"hits": [],
"out_of": 0,
"page": 1,
"request_params": {
"collection_name": "churches",
"first_q": "geb",
"per_page": 10,
"q": "geb"
},
"search_cutoff": false,
"search_time_ms": 0
}Fanis Tharropoulos
08/12/2025, 8:00 AMnpm list axios
solomon mulatu
08/12/2025, 8:02 AMsolomon mulatu
08/12/2025, 8:05 AMFanis Tharropoulos
08/12/2025, 9:10 AMlist
command it will show you the exact version that you have installed.solomon mulatu
08/12/2025, 9:22 AMFanis Tharropoulos
08/12/2025, 9:34 AMsolomon mulatu
08/12/2025, 10:01 AMimport { Dimensions, TouchableOpacity, View, StyleSheet, ScrollView } from 'react-native';
import { InstantSearch, useHits } from 'react-instantsearch-core';
import typesenseInstantsearchAdapter from '@/lib/typesense.adapter';
import SearchBox from '../box';
import { useAppTheme } from '@/utils/useAppTheme';
import { useState } from 'react';
import { useFontScaling } from '@/hooks/useFontScaling';
import { useRouter } from 'expo-router';
import { TapGestureHandler } from 'react-native-gesture-handler';
import Image from '@/components/elements/Image';
import { Text } from '@/components/elements/Text';
import { fonts, ThemedStyle } from '@/theme';
import { getFontSize } from '@/utils/font';
import SearchResult from '../results';
import { SafeAreaView } from 'react-native-safe-area-context';
import theme from '@/providers/theme';
import createTypesenseAdapter from '@/lib/typesense.adapter';
import { DataPersistKeys, useDataPersist } from '@/hooks/useDataPersist';
const screenWidth = Dimensions.get('window').width;
// Define TypeScript interface for church data
interface Church {
id: number;
title: string;
images: any[];
}
const SearchResultItem: React.FC = ({ hit, index }) => {
const { themed, theme } = useAppTheme();
const styles = themed(themedlayoutStyles);
const [currentPage, setCurrentPage] = useState<number>(0);
const fontProps = useFontScaling();
const router = useRouter();
const distanceInKm = (hit.geo_distance_meters.location / 1000).toFixed(2);
const handlePress = () => {
router.push('/church/profile');
};
return (
<View style={[
index % 2 ? { paddingLeft: 10 } : { paddingRight: 10 }, { flex: 1 },
styles.churchItemContainer,
]}>
{/* TapGestureHandler around PagerView */}
<TapGestureHandler onActivated={handlePress}>
<View style={styles.churchCard}>
<View style={styles.imageWrapper}>
<View style={styles.imagePager}>
<Image style={styles.churchImage} source={require('@/assets/images/nearby-church.png')} contentFit="cover" />
</View>
</View>
</View>
</TapGestureHandler>
{/* Text section */}
<TouchableOpacity onPress={handlePress} activeOpacity={0.8}>
<View style={styles.churchInfoContainer}>
<View style={styles.churchTitleContainer}>
<Text
{...fontProps}
style={[styles.churchTitle, { flex: 1 }]}
numberOfLines={1}
>
{hit.name_en}
</Text>
</View>
<Text
{...fontProps}
style={styles.churchDistance}
numberOfLines={1}
>
{distanceInKm} km away
</Text>
</View>
</TouchableOpacity>
</View>
);
};
export default function layout() {
const { theme, themed } = useAppTheme();
const styles = themed(themedlayoutStyles);
const { getPersistData, setPersistData, removeAllPersistData } = useDataPersist();
const [searchClient, setSearchClient] = useState<any>(null);
// Fetch location and set search client
useState(() => {
(async () => {
const storedLocation = await getPersistData<{ latitude: number, longitude: number }>(DataPersistKeys.USER_LOCATION);
const latitude = storedLocation?.latitude ?? 0;
const longitude = storedLocation?.longitude ?? 0;
setSearchClient(createTypesenseAdapter(latitude, longitude).searchClient);
})();
});
return (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView
style={styles.root}
contentContainerStyle={{ paddingBottom: theme.spacing.xxl }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{searchClient && (
<InstantSearch indexName="churches" searchClient={searchClient}>
<SearchBox />
<SearchResult hitComponent={SearchResultItem} />
</InstantSearch>
)}
</ScrollView>
</SafeAreaView>
);
}
const themedlayoutStyles: ThemedStyle<any> = theme =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: theme.colors.background,
},
churchItemContainer: {
marginBottom: theme.spacing.md,
paddingHorizontal: theme.spacing.md,
width: '100%',
},
churchCard: {
height: 90,
width: '100%',
position: 'relative',
overflow: 'hidden',
backgroundColor: theme.colors.componentBackground,
borderTopLeftRadius: 15,
borderTopRightRadius: 15,
},
imageWrapper: {
flex: 1,
overflow: 'hidden',
},
imagePager: {
flex: 1,
},
churchImage: {
width: '100%',
height: '100%',
},
churchInfoContainer: {
paddingTop: theme.spacing.xs,
backgroundColor: theme.colors.componentBackground,
borderBottomLeftRadius: 15,
borderBottomRightRadius: 15,
paddingLeft: 15
},
churchTitleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
churchTitle: {
fontFamily: fonts.roboto.regular,
color: theme.colors.text.default,
fontSize: getFontSize(14),
flex: 1,
},
churchDistance: {
fontFamily: fonts.roboto.bold,
color: theme.colors.text.default,
fontSize: getFontSize(12),
paddingBottom: theme.spacing.xs,
},
});
//SearchBox
import { TextInput, View, StyleSheet } from 'react-native';
import { useFocusEffect, } from 'expo-router';
import { useAppTheme } from '@/utils/useAppTheme';
import { fonts, ThemedStyle } from '@/theme';
import { getFontSize } from '@/utils/font';
import FilterIcon from '@/components/icons/explore/Filter';
import { useCallback, useRef, useState } from 'react';
import { useSearchBox } from 'react-instantsearch-core';
export default function SearchBox(props) {
const { themed } = useAppTheme();
const styles = themed(themedHomeStyles);
const inputRef = useRef<TextInput>(null);
// Ensure focus on every screen visit
const focusInput = useCallback(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 100); // Small delay to ensure input is ready
}, []);
useFocusEffect(
useCallback(() => {
focusInput();
}, [focusInput]),
);
const { query, refine } = useSearchBox(props);
const [inputValue, setInputValue] = useState(query);
function setQuery(newQuery) {
setInputValue(newQuery);
refine(newQuery);
}
// Track when the InstantSearch query changes to synchronize it with
// the React state.
// We bypass the state update if the input is focused to avoid concurrent
// updates when typing.
if (query !== inputValue && !inputRef.current?.isFocused()) {
setInputValue(query);
}
return (
<View style={styles.searchContainer}>
<View style={styles.searchBox}>
<TextInput
ref={inputRef}
style={styles.searchInput}
placeholder="Search for places, events, people"
placeholderTextColor="rgba(0, 0, 0, 0.6)"
autoFocus={true}
value={inputValue} // ✅ control the input
onChangeText={setQuery} // ✅ update Algolia query
/>
<FilterIcon height={24} width={24} />
</View>
</View>
);
}
const themedHomeStyles: ThemedStyle<any> = theme =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: theme.colors.background,
},
searchContainer: {
height: theme.spacing.searchBox,
marginTop: theme.spacing.lg,
paddingHorizontal: theme.spacing.md,
},
searchBox: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.lg,
backgroundColor: theme.colors.componentBackground,
borderRadius: 50,
height: 46,
},
searchInput: {
flex: 1,
fontFamily: fonts.roboto.regular,
fontSize: getFontSize(14),
color: theme.colors.text.default,
},
});Fanis Tharropoulos
08/12/2025, 10:20 AMsolomon mulatu
08/12/2025, 10:33 AMFanis Tharropoulos
08/12/2025, 10:35 AMsolomon mulatu
08/12/2025, 10:37 AMFanis Tharropoulos
08/12/2025, 10:58 AMsolomon mulatu
08/12/2025, 10:59 AMFanis Tharropoulos
08/12/2025, 11:03 AMsolomon mulatu
08/12/2025, 11:51 AMFanis Tharropoulos
08/12/2025, 12:30 PM