Skip to content

Instantly share code, notes, and snippets.

@sahilkashyap64
Created April 30, 2024 07:55
Show Gist options
  • Save sahilkashyap64/fc16c1c2cc890d8376d6089355dd7f45 to your computer and use it in GitHub Desktop.
Save sahilkashyap64/fc16c1c2cc890d8376d6089355dd7f45 to your computer and use it in GitHub Desktop.
survey react native
import React,{useRef,useEffect} from 'react';
import {View,Text,SafeAreaView,ScrollView} from 'react-native'
import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
import {GOOGLE_MAPS_AUTOCOMPLETE_API_KEY} from '../../constants'
const apiKey=GOOGLE_MAPS_AUTOCOMPLETE_API_KEY;
import { getReverseCodeLocation } from '../../api/locations';
navigator.geolocation = require('react-native-geolocation-service');
const AddressPicker = ({onSelect,latitude,longitude}) => {
console.log("AddressPicker",{latitude,longitude});
const originRef = useRef();
useEffect(() => {
console.log({latitude,longitude});
originRef.current?.setAddressText('Fetching location');
}, []);
useEffect(() => {
console.log("handlePlaceSelect");
handlePlaceSelect(latitude,longitude);
}, [latitude!==0 && longitude!==0]);
const handlePlaceSelect = async (latitude, longitude) => {
try {
if (latitude && longitude) {
// console.log("details", details.name + ", " + details.formatted_address);
// Call the getReverseCodeLocation function with latitude, longitude, and optional token
const response = await getReverseCodeLocation(latitude, longitude);
console.log("handlePlaceSelect response",response);
if (response.ok) {
const locationData = await response.json();
// console.log({locationData:locationData.results[0]});
const address = locationData.results[0].formatted_address;
originRef.current?.setAddressText(address);
onSelect(locationData.results[0]);
} else {
console.error("Error fetching reverse geocode location:", response.statusText);
originRef.current?.setAddressText('Error');
// Handle the error accordingly
}
}
} catch (error) {
console.error("Error handling place selection:", error);
// originRef.current?.setAddressText('Error');
// Handle the error accordingly
}
};
return (<View>
<ScrollView keyboardShouldPersistTaps="handled">
<View keyboardShouldPersistTaps="handled" >
<GooglePlacesAutocomplete
ref={originRef}
currentLocation={true}
placeholder='Enter your location'
minLength={2} // minimum length of text to search
autoFocus={true}
returnKeyType={'search'} // Can be left out for default return key https://facebook.github.io/react-native/docs/textinput.html#returnkeytype
keyboardAppearance={'light'} // Can be left out for default keyboardAppearance https://facebook.github.io/react-native/docs/textinput.html#keyboardappearance
listViewDisplayed='true' // true/false/undefined
fetchDetails={true}
enableHighAccuracyLocation={true}
//renderDescription={row => row.description || row.formatted_address || row.name}
// renderDescription={(row) => row.description} // custom description render
//components='country:ind'
onPress={(data, details = null) => {
//let address = details.formatted_address.split(', ');
// console.log("DATAA", data);
// console.log("details", details);
console.log("details", details.name + ", " + details.formatted_address);
if (data && details) {
console.log("data of placws",data);
onSelect(details);
// this.setState({
// disLocation: data.description ? data.description : details.name + ", " + details.formatted_address,
// lat: details.geometry.location.lat ? details.geometry.location.lat : data.geometry.location.lat,
// lng: details.geometry.location.lng ? details.geometry.location.lng : data.geometry.location.lng
// })
}
}}
nearbyPlacesAPI='GoogleReverseGeocoding'
GooglePlacesSearchQuery={{
fields: "geometry",
// available options for GooglePlacesSearch API : https://developers.google.com/places/web-service/search
rankby: 'distance',
// type: 'cafe'
}}
getDefaultValue={() => ''}
query={{
// available options: https://developers.google.com/places/web-service/autocomplete
key: apiKey,
//language: 'fr', // language of the results
//types: 'address', // default: 'geocode'
// components: 'country:ca' // added manually
}}
styles={{
textInputContainer: {
width: '100%'
}
}}
textInputProps={{ onBlur: () => {} }}
//GooglePlacesDetailsQuery={{ fields: 'formatted_address' }}
GooglePlacesDetailsQuery={{ fields: ['geometry', 'formatted_address'] }}
debounce={300} // debounce the requests in ms. Set to 0 to remove debounce. By default 0ms.
/>
</View>
</ScrollView>
</View>);
};
export default AddressPicker;
import React, { useState, useRef,useEffect } from 'react';
import { StyleSheet, ScrollView, Text, TextInput, View,SafeAreaView,TouchableOpacity } from 'react-native';
import ProfileTopMenu from "../../../components/ProfileTopMenu/index";
import ProgressIndicator from "../../../components/ProgressIndicator/index";
// import { SimpleSurvey } from 'react-native-simple-surveyQuestions';
import { SimpleSurvey } from './simple-survey';
import { ScreenNames } from '../../index';
import { debounce,cloneDeep } from 'lodash';
import MiniLoader from '../../../components/Loaders/MiniLoader';
import useAuth from '../../../hooks/useAuth';
import usePlayer from '../../../hooks/usePlayer';
import Sheet from '../../../components/BottomSheet';
import Colors from '../../../theme/colors';
import Typography from '../../../theme/typography';
import Button from '../../../components/Button';
const GREEN = '#4CAF50';
const LIGHT_GRAY = '#D3D3D3';
const DARK_GRAY = '#555';
const WHITE = '#FFF';
import Icon from 'react-native-vector-icons/Ionicons'; // Make sure to install react-native-vector-icons
import { survey } from './surveyJson';
import {SCREEN_HEIGHT, normalize} from '../../../theme/metrics';
import { useRecoilState } from 'recoil';
import { surveyQuestionsState } from '../../../store/atoms';
import Geolocation from 'react-native-geolocation-service';
const PlayerAvailabilityScreen = ({ navigation, route }) => {
const surveyWithAnswers = route?.params?.survey||false;
const {user} = useAuth();
const {
useSurvey,
} = usePlayer();
const [backgroundColor, setBackgroundColor] = useState(LIGHT_GRAY);
const [answersSoFar, setAnswersSoFar] = useState('');
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); // Added state for current question index
const surveyRef = useRef(null);
const sheetRef = useRef();
const { fetchSurveyQuestions } = useSurvey();
const [surveyQuestions, setSurveyQuestions] = useRecoilState(surveyQuestionsState);
const [locationLoader, setLocationLoader] = useState(true);
const [visible, setvisible] = useState(false);
const [isLoading, setIsLoading] = useState(true); // New state variable to track loading status
const [userPos, setUserPos] = useState({
latitude: 0,
longitude: 0,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
useEffect(() => {
const loadQuestions = async () => {
if (route.params?.survey) {
// Use survey questions from route if available
setSurveyQuestions(route.params.survey);
setIsLoading(false);
} else {
// Fetch survey questions if not passed through route params
await fetchSurveyQuestions(user.session.access_token);
setIsLoading(false);
}
};
loadQuestions();
}, []);
useEffect(() => {
const checkLocationPermission = async () => {
if (surveyQuestions.some(question => question.questionType === 'AddressPicker')) {
await requestLocationPermission();
}
};
if (!isLoading) {
checkLocationPermission();
}
}, [!isLoading, surveyQuestions]);
const debouncedUserPosUpdate = debounce((position) => {
const crd = position.coords;
setUserPos({
latitude: crd.latitude,
longitude: crd.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
}, 1000); // Adjust the milliseconds as needed
// useEffect(() => {
// console.log("requestLocationPermission useEffect","surveyQuestions.length",surveyQuestions.length);
// requestLocationPermission();
// }, [surveyQuestions.length!==0]);
const getCurrentLocation = () => {
setLocationLoader(true);
Geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
try {
debouncedUserPosUpdate(position);
setLocationLoader(false);
} catch (error) {
console.error('Error getting location name:', error);
}
},
(error) => {
setLocationLoader(false);
console.log(error.code, error.message);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 3600000 }
);
};
const requestLocationPermission = async () => {
try {
console.log("requestLocationPermission");
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'This app needs access to your location to show you nearby coaches.',
buttonPositive: 'OK',
}
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
// Permission granted, get the user's location
getCurrentLocation();
} else {
console.log('Location permission denied');
}
} else if (Platform.OS === 'ios') {
const whenInUseGranted = await Geolocation.requestAuthorization('whenInUse');
const alwaysGranted = await Geolocation.requestAuthorization('always');
if (whenInUseGranted === 'granted' || alwaysGranted === 'granted') {
getCurrentLocation();
} else {
console.log('Location permission denied');
}
}
} catch (err) {
console.warn(err);
}
};
const onSurveyFinished = (answers) => {
// console.log("surveyQuestions json",JSON.stringify(surveyQuestions));
// console.log("onSurveyFinished json",JSON.stringify(answers));
const infoQuestionsRemoved = answers.filter((elem) => elem.questionType !== 'Info');
// First, filter out 'Info' type questions from the answers
const nonInfoAnswers = answers.filter((answer) => {
const questionDetails = surveyQuestions.find((question) => question.questionId === answer.questionId);
return questionDetails.questionType !== 'Info';
});
// Then, enrich the remaining answers with their corresponding questionType
const answersWithQuestionType = nonInfoAnswers.map((answer) => {
const questionDetails = surveyQuestions.find((question) => question.questionId === answer.questionId);
// Return a new object that includes all properties of the answer plus the questionType
return { ...answer, questionType: questionDetails.questionType,questionText:questionDetails.questionText };
});
// console.log("Answers with question type (excluding 'Info'):", JSON.stringify(answersWithQuestionType));
setvisible(true);
// console.log("currentQuestionIndex",currentQuestionIndex,"surveylength",surveyQuestions.length);
sheetRef.current.open();
navigation.navigate(ScreenNames.SURVEY_COMPLETED, { surveyAnswers: answersWithQuestionType });
};
const onAnswerSubmitted = (answer) => {
setAnswersSoFar(JSON.stringify(surveyRef.current && surveyRef.current.getAnswers(), null, 2));
switch (answer.questionId) {
case 'playerName':
// You can perform additional logic based on the player's name
break;
case 'playingSurface':
// You can perform additional logic based on the preferred playing surface
break;
default:
break;
}
};
const renderPreviousButton = (onPress, enabled) => (
<View style={{ flexGrow: 1, maxWidth: 100, marginTop: 10, marginBottom: 10 }}>
<Button color={DARK_GRAY} onPress={onPress} loading = {!enabled} disabled={!enabled} title="Previous" externalTextstyle={styles.buttonStyle} externalButtonStyle={{}}/>
</View>
);
const renderNextButton = (onPress, enabled) => (
<View style={{ flexDirection: 'row' }}>
{currentQuestionIndex < surveyQuestions.length - 1 ? (
<>
<View style={{ flexGrow: 1, maxWidth: 100, marginTop: 10, marginBottom: 10 }}>
<Button color={DARK_GRAY} onPress={onPress} disabled={!enabled} loading={!enabled} title="Next" externalTextstyle={styles.buttonStyle}/>
</View>
{/* {currentQuestionIndex > 0 && renderSkipButton(() => onSkipPress(), enabled)} */}
</>
) : (
<View style={{ flexGrow: 1, maxWidth: 100, marginTop: 10, marginBottom: 10 }}>
<Button color={GREEN} onPress={onPress} disabled={!enabled} loading={!enabled} title="Finish" externalTextstyle={styles.buttonStyle}/>
</View>
)}
</View>
);
const renderButton = (data, index, isSelected, onPress) => (
<TouchableOpacity
key={`selection_button_${index}`}
onPress={onPress}
style={{
marginTop: 10,
marginBottom: 10,
backgroundColor: '#FFFFFF',
padding: 15,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 5,
// borderWidth: isSelected ? 2 : 1,
// borderColor: isSelected ? GREEN : DARK_GRAY,
}}
>
<Icon
name={isSelected ? 'checkmark-circle' : 'ellipse-outline'}
size={30}
color={isSelected ? 'green' : '#D1D1D6'}
style={{ marginRight: 10 }}
/>
<View style={{ flex: 1 }}>
<Text style={styles.optionTextStyle}>
{data.optionText}
</Text>
{data.description && (
<Text style={styles.descriptionStyle}>
{data.description}
</Text>
)}
</View>
</TouchableOpacity>
);
const renderQuestionText = (questionText, index, total) => (
<View style={styles.questionContainer}>
{/* <Text style={styles.questionIndicator}>{`${index + 1}/${total}`}</Text> */}
<Text style={styles.questionText}>{questionText}</Text>
<Text style={styles.questionSubText}>{surveyQuestions[index].subText}</Text>
{/* {surveyQuestions[index].subText??<Text style={styles.questionSubText}>{surveyQuestions[index].subText}</Text>} */}
</View>
);
const renderTextBox = (onChange, value, placeholder, onBlur) => (
<View style={{ flex: 1, justifyContent: 'flex-start' }}>
<TextInput
style={styles.textBox}
onChangeText={(text) => onChange(text)}
numberOfLines={1}
placeholder={placeholder}
placeholderTextColor={DARK_GRAY}
value={value}
multiline
onBlur={onBlur}
blurOnSubmit
returnKeyType="done"
/>
</View>
);
const renderFinishedButton = (onPress, enabled) => (
<View style={{ flexGrow: 1, maxWidth: 100, marginTop: 10, marginBottom: 10 }}>
<Button title="Finish" onPress={onPress} disabled={!enabled} color={GREEN} />
</View>
);
const renderNumericInput = (onChange, value, placeholder, onBlur) => (
<View style={{ flex: 1, justifyContent: 'flex-start' }}>
<TextInput
style={styles.numericInput}
onChangeText={(text) => onChange(text)}
placeholder={placeholder}
placeholderTextColor={DARK_GRAY}
value={String(value)}
keyboardType="numeric"
onBlur={onBlur}
maxLength={10}
/>
</View>
);
const renderInfoText = (infoText) => (
<View style={{ margin: 10,
// backgroundColor:'skyblue',
alignItems:'center',
flex: 1, justifyContent: 'flex-start' }}>
<Text style={styles.infoText}>{infoText}</Text>
<Text style={styles.questionSubText}>{surveyQuestions[currentQuestionIndex].subText}</Text>
</View>
);
const getDeepCopyOfSurveyQuestions = () => {
// console.log({surveyWithAnswers:surveyWithAnswers.length,
// surveyQuestions:surveyQuestions.length,
// surveyQuestions:surveyQuestions.length});
// LOG When surveyWithAnswers has no data {"surveyQuestions": 0, "surveyQuestions": 4, "surveyWithAnswers": undefined}
//LOG When surveyWithAnswers has data {"surveyQuestions": 4, "surveyQuestions": 0, "surveyWithAnswers": 4}
return cloneDeep(surveyQuestions);
};
console.log("surveyWithAnswers length",JSON.stringify(surveyWithAnswers.length));
console.log("surveyQuestions length",JSON.stringify(surveyQuestions.length));
// console.log("surveyQuestions",JSON.stringify(surveyQuestions.length));
if (isLoading) {
return (<MiniLoader subtext={'Fetching questions...'} /> );
}
return ( <SafeAreaView style={styles.safeArea}>
<View style={styles.mainContainer}>
<ProfileTopMenu title={"Share Your Availability"} actionTitle={""} onBack={() => navigation.pop()} />
<ScrollView style={styles.container}
keyboardShouldPersistTaps="always"
contentContainerStyle={{ flexGrow: 1, justifyContent: 'space-between', flexDirection: 'column' }}
>
<><View style={styles.questionIndicatorContainer}>
{/* <Text style={styles.questionIndicator}>{`${currentQuestionIndex + 1}/${surveyQuestions.length}`}</Text> */}
<ProgressIndicator current={currentQuestionIndex+1} total={surveyQuestions.length} />
</View>
<SimpleSurvey
ref={(ref) => { surveyRef.current = ref; }}
survey={getDeepCopyOfSurveyQuestions()}
renderSelector={renderButton}
containerStyle={styles.surveyContainer}
selectionGroupContainerStyle={styles.selectionGroupContainer}
navButtonContainerStyle={styles.navButtonContainer}
renderPrevious={currentQuestionIndex==0?null:renderPreviousButton}
renderNext={renderNextButton}
renderFinished={renderFinishedButton}
renderQuestionText={(text) => renderQuestionText(text, currentQuestionIndex, surveyQuestions.length)}
onSurveyFinished={onSurveyFinished}
onAnswerSubmitted={(answer) => {
onAnswerSubmitted(answer);
}}
renderTextInput={renderTextBox}
renderNumericInput={renderNumericInput}
renderInfo={renderInfoText}
onCurrentQuestionIndexChange={(currentIndex) => {
console.log(`Current question: ${currentIndex} of ${surveyQuestions.length}`);
setCurrentQuestionIndex(currentIndex);
}}
userPos={userPos}
/></>
</ScrollView>
</View>
<Sheet
sheetRef={sheetRef}
height={350}
setvisible={visible}
showButton={true}
title="Warning"
>
<View style={{display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
margin: normalize(10),
marginVertical:normalize(20)}}>
<View >
<Text style={{
color: Colors.theme.gray3,
// padding: normalize(7),
fontSize: Typography.fontSize.small,
fontFamily: Typography.fontFamily.montserratMedium,}}>
By signing up to this service you agree to share you contact details with other Tennis Plan members. You can remove yourself from the player match service at anytime from your profile settings.
</Text>
</View>
</View>
</Sheet>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#F6F7FB',
},
mainContainer: {
flex: 1,
},
container: {
flex: 1,
// backgroundColor:'blue'
},
surveyContainer: {
paddingHorizontal: 20, // Adjust padding as needed
paddingBottom: 20, // Extra padding for bottom navigation buttons
// backgroundColor:'red',
flexGrow:1,
},
questionIndicatorContainer: {
// backgroundColor:'yellow',
alignItems: 'center', // Center the question indicator horizontally
marginTop: 10, // Adjust margin as needed
},
questionIndicator: {
fontSize: 20,
fontWeight: 'bold',
color: GREEN,
},
buttonStyle:{
fontFamily: Typography.fontFamily.montserratBold,
fontSize: Typography.fontSize.medium,
color: Colors.theme.white,
},
answersContainer: {
flex: 1,
marginTop: 20,
padding: 20,
backgroundColor: LIGHT_GRAY,
},
selectionGroupContainer: {
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'flex-start',
},
navButtonContainer: {
// backgroundColor:'green',
flexDirection: 'row',
// alignItems:'baseline',
// justifyContent: 'space-between',
justifyContent: 'space-around',
// alignSelf: 'flex-end',
},
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
questionContainer: {
flexDirection: 'column',
alignItems: 'center',
alignSelf:'center',
margin: 10,
// backgroundColor:'purple',
},
questionIndicator: {
fontSize: 20,
fontWeight: 'bold',
color: DARK_GRAY,
marginRight: 10,
},
questionText: {
fontSize: 18,
color: DARK_GRAY,
},
questionSubText: {
fontSize: 10,
color: DARK_GRAY,
},
textBox: {
borderWidth: 1,
borderColor: DARK_GRAY,
borderRadius: 10,
padding: 10,
margin: 10,
},
numericInput: {
borderWidth: 1,
borderColor: DARK_GRAY,
borderRadius: 10,
padding: 10,
margin: 10,
},
infoText: {
fontSize: 18,
color: DARK_GRAY,
},
buttonWrapper: {
backgroundColor:'pink',
marginTop: 10,
marginBottom: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
nextButton: {
backgroundColor: GREEN, // This is the button's background color
borderRadius: 20, // Adjust for rounded corners
paddingVertical: 15, // Adjust for vertical padding
paddingHorizontal: 30, // Adjust for horizontal padding
alignItems: 'center', // Centers the text in the button
justifyContent: 'center', // Centers the text in the button
},
nextButtonText: {
color: 'white', // This is the text color
fontSize: 16, // Adjust the font size as needed
fontWeight: 'bold', // Adjust the font weight as needed
},
descriptionStyle: {
color: Colors.theme.gray3,
// padding: normalize(7),
fontSize: Typography.fontSize.mini,
fontFamily: Typography.fontFamily.montserratMedium,
},
optionTextStyle: {
// width: '90%',
fontFamily: Typography.fontFamily.montserratMedium,
fontSize: Typography.fontSize.small,
color: Colors.theme.black,
// padding: normalize(20),
},
profileContainer:{display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
margin: normalize(10),
marginVertical:normalize(20)}
});
export default PlayerAvailabilityScreen;
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const ProgressIndicator = ({ current, total }) => {
// Format numbers as two digits
const formattedCurrent = String(current).padStart(2, '0');
const formattedTotal = String(total).padStart(2, '0');
return (
<View style={styles.container}>
<View style={styles.circle}>
<Text style={styles.text}>{`${formattedCurrent}/${formattedTotal}`}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
// Container styles if needed
},
circle: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: '#53B17A',
borderColor: 'rgba(83, 177, 122, 0.2)',
borderWidth: 6,
borderRadius: 30,
},
text: {
color: 'white', // Text color
fontSize: 18, // Text size
fontWeight: 'bold', // Text weight
},
});
export default ProgressIndicator;
// seeds/insert_initial_data.js
exports.seed = async function(knex) {
// Deletes ALL existing entries in reverse dependency order, then inserts new data
await knex.transaction(async trx => {
await trx('question_options').del();
await trx('option_choices').del();
await trx('questions').del();
await trx('survey_sections').del();
await trx('survey_headers').del();
await trx('input_types').del();
await trx('option_groups').del();
await trx('organizations').del();
await trx('organizations').insert([
{ id: 1, organization_name: 'Tennis Survey Organization' }
]);
await trx('survey_headers').insert([
{ id: 1, organization_id: 1, survey_name: 'Tennis Partner Survey', instructions: 'Please complete this survey to find your ideal tennis partner.' }
]);
const inputTypes = [
{ id: 1, input_type_name: 'Info' },
{ id: 2, input_type_name: 'TextInput' },
{ id: 3, input_type_name: 'NumericInput' },
{ id: 4, input_type_name: 'SelectionGroup' },
{ id: 5, input_type_name: 'MultipleSelectionGroup' },
{ id: 6, input_type_name: 'ConditionalSelectionWithText' },
{ id: 7, input_type_name: 'AddressPicker' },
{ id: 8, input_type_name: 'MultiAddressPicker' }
];
await trx('input_types').insert(inputTypes);
const optionGroups = [
{ id: 1, option_group_name: 'Playing Level' },
{ id: 2, option_group_name: 'Favorite Tennis Players' },
{ id: 3, option_group_name: 'Looking For' },
{ id: 4, option_group_name: 'Gender Options' }
];
await trx('option_groups').insert(optionGroups);
const optionChoices = [
{ id: 1, option_group_id: 1, option_choice_name: 'Beginner (NTRP 2.5)', description: 'Just getting started in the game', sort_order: 1 },
{ id: 2, option_group_id: 1, option_choice_name: 'Advanced Beginner (NTRP 3.0)', description: 'I can rally but my strokes are not consistent yet', sort_order: 2 },
{ id: 3, option_group_id: 1, option_choice_name: 'Intermediate (NTRP 3.5)', description: 'I can hit with spin and direction most of the time', sort_order: 3 },
{ id: 4, option_group_id: 1, option_choice_name: 'Advanced (NTRP 4.0)', description: 'I can consistently rally with spin, direction, and pace', sort_order: 4 },
{ id: 5, option_group_id: 1, option_choice_name: 'Advanced Plus (NTRP 4.5)', description: 'I have control over my shots and hit consistently with depth and pace', sort_order: 5 },
{ id: 6, option_group_id: 1, option_choice_name: 'Expert (NTRP 5.0)', description: 'I have competitive experience and advanced skill levels', sort_order: 6 },
{ id: 7, option_group_id: 2, option_choice_name: 'Roger Federer', description: 'A legendary player known for his precision and grace', sort_order: 1 },
{ id: 8, option_group_id: 2, option_choice_name: 'Serena Williams', description: 'Famed for her powerful serve and determination', sort_order: 2 },
{ id:9, option_group_id:2, option_choice_name: 'Novak Djokovic', description: 'Renowned for his exceptional agility and endurance', sort_order:3},
{ id:10, option_group_id:2, option_choice_name: 'Naomi Osaka', description: 'Admired for her powerful gameplay and strategic acumen', sort_order:4},
{ id:11, option_group_id:2, option_choice_name: 'Rafael Nadal', description: 'Celebrated for his dominance on clay courts and fighting spirit',sort_order: 5},
{ id: 12, option_group_id: 3, option_choice_name: 'Fun / social', description: 'Looking to enjoy the game and meet new people', sort_order: 1 },
{ id: 13, option_group_id: 3, option_choice_name: 'Casual hitting', description: 'Interested in playing tennis casually without competitive pressure', sort_order: 2 },
{ id: 14, option_group_id: 3, option_choice_name: 'Friendly competition', description: 'Seeking a bit of competition while having fun', sort_order: 3 },
{ id: 15, option_group_id: 3, option_choice_name: 'High level competition', description: 'Aiming for serious, competitive matches', sort_order: 4 },
{ id: 16,option_group_id: 4, option_choice_name: 'Male', description: 'Male', sort_order: 1 },
{ id: 17,option_group_id: 4, option_choice_name: 'Female', description: 'Female', sort_order: 2 },
{ id: 18,option_group_id: 4, option_choice_name: 'Other', description: 'Other', sort_order: 3 }
];
await trx('option_choices').insert(optionChoices);
await trx('survey_sections').insert([
{ id: 1, survey_header_id: 1, section_name: 'Main Section', section_title: 'Survey Questions', section_required: true }
]);
const questions = [
{ id: 1, survey_section_id: 1, input_type_id: 2, question_text: 'What is your name?', answer_required: true, option_group_id: null },
{ id: 2, survey_section_id: 1, input_type_id: 7, question_text: 'Location', answer_required: true, option_group_id: null },
{ id: 3, survey_section_id: 1, input_type_id: 4, question_text: 'Choose your level?', answer_required: true, option_group_id: 1 },
{ id: 4, survey_section_id: 1, input_type_id: 5, question_text: 'Select two or three of your favorite tennis players!', answer_required: true, option_group_id: 2 },
{ id: 5, survey_section_id: 1, input_type_id: 5, question_text: 'You are looking for...', answer_required: true, option_group_id: 3 },
{ id: 6, survey_section_id: 1, input_type_id: 6, question_text: 'What is your gender?', answer_required: true, option_group_id: 4 },
{ id: 7, survey_section_id: 1, input_type_id: 8, question_text: 'What is your closest tennis court? (you may choose more than one)', answer_required: true }
];
await trx('questions').insert(questions);
const questionOptions = [
//'Choose your level?'
{ id: 1, question_id: 3, option_choice_id: 1 },
{ id: 2, question_id: 3, option_choice_id: 2 },
{ id: 3, question_id: 3, option_choice_id: 3 },
{ id: 4, question_id: 3, option_choice_id: 4 },
{ id: 5, question_id: 3, option_choice_id: 5 },
{ id: 6, question_id: 3, option_choice_id: 6 },
// For 'Select two or three of your favorite tennis players'
{ id: 7,question_id: 4, option_choice_id: 7 },
{ id: 8,question_id: 4, option_choice_id: 8 },
{id: 9, question_id: 4, option_choice_id: 9 },
{ id: 10,question_id: 4, option_choice_id: 10 },
{ id: 11,question_id: 4, option_choice_id: 11 },
// For 'You are looking for...'
{id: 12, question_id: 5, option_choice_id: 12 },
{ id: 13,question_id: 5, option_choice_id: 13 },
{ id: 14,question_id: 5, option_choice_id: 14 },
{id: 15, question_id: 5, option_choice_id: 15 },
// For 'What is your gender?'
{ id: 16,question_id: 6, option_choice_id: 16 },
{ id: 17,question_id: 6, option_choice_id: 17 },
{ id: 18,question_id: 6, option_choice_id: 18 },
];
await trx('question_options').insert(questionOptions);
});
};
import React, { Component } from 'react';
import {
View,
ViewPropTypes,
} from 'react-native';
import PropTypes from 'prop-types';
import AddressPicker from '../../../components/AddressPicker';
import TennisCourtSearch from '../../../components/TennisCourtPicker';
import SelectionGroup, { SelectionHandler } from 'react-native-selection-group';
export class SimpleSurvey extends Component {
static propTypes = {
survey: PropTypes.arrayOf(
PropTypes.shape({
questionType: PropTypes.string.isRequired,
questionText: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
questionId: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
optionText: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
}))
}).isRequired
).isRequired,
onAnswerSubmitted: PropTypes.func,
onSurveyFinished: PropTypes.func,
renderSelector: PropTypes.func,
renderTextInput: PropTypes.func,
selectionGroupContainerStyle: ViewPropTypes.style,
containerStyle: ViewPropTypes.style,
renderPrev: PropTypes.func,
renderNext: PropTypes.func,
renderFinished: PropTypes.func,
renderInfo: PropTypes.func,
autoAdvance: PropTypes.bool,
onCurrentQuestionIndexChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = { currentQuestionIndex: 0, answers: [] };
this.updateAnswer.bind(this);
this.selectionHandlers = [];
}
componentDidUpdate(prevProps, prevState) {
// Check if currentQuestionIndex has changed
if (prevState.currentQuestionIndex !== this.state.currentQuestionIndex) {
// Check if the callback prop is provided
if (this.props.onCurrentQuestionIndexChange) {
// Call the callback with the new currentQuestionIndex
this.props.onCurrentQuestionIndexChange(this.state.currentQuestionIndex);
}
}
}
getAnswers() {
const filteredAnswers = this.state.answers.filter(n => n);
return filteredAnswers;
}
// This function returns true if all the condition have been met for a multiple selection question.
validateMultipleSelectionSurveyAnswers() {
const { currentQuestionIndex, answers } = this.state;
if (!this.props.survey[currentQuestionIndex].questionType === 'MultipleSelectionGroup') {
throw new Error(
'validateMultipleSelectionSurveyAnswers was asked to validate a non MultipleSelectionGroup item'
);
}
let maxMultiSelect = 1;
let minMultiSelect = 1;
if (this.props.survey[currentQuestionIndex].questionSettings.maxMultiSelect) {
maxMultiSelect = Number(this.props.survey[currentQuestionIndex].questionSettings.maxMultiSelect);
}
if (this.props.survey[currentQuestionIndex].questionSettings.minMultiSelect) {
minMultiSelect = Number(this.props.survey[currentQuestionIndex].questionSettings.minMultiSelect);
} else {
minMultiSelect = maxMultiSelect;
}
if (answers[currentQuestionIndex] && answers[currentQuestionIndex].value.length >= minMultiSelect) {
return true;
} else { return false; }
}
updateAnswer(answerForCurrentQuestion) {
const { answers } = this.state;
answers[this.state.currentQuestionIndex] = answerForCurrentQuestion;
this.setState({ answers });
}
// Do what the next or finished button normally do.
autoAdvance() {
const { answers } = this.state;
const { survey } = this.props;
let { currentQuestionIndex } = this.state;
if (survey[currentQuestionIndex].questionType === 'MultipleSelectionGroup'
&& !this.validateMultipleSelectionSurveyAnswers()) {
return;
}
if (currentQuestionIndex === this.props.survey.length - 1) {
if (this.props.onAnswerSubmitted && answers[currentQuestionIndex]) {
this.props.onAnswerSubmitted(answers[currentQuestionIndex]);
}
if (this.props.onSurveyFinished) {
// Remove empty answers, coming from info screens.
const filteredAnswers = answers.filter(n => n);
this.props.onSurveyFinished(filteredAnswers);
}
} else {
if (this.props.onAnswerSubmitted && answers[currentQuestionIndex]) {
this.props.onAnswerSubmitted(answers[currentQuestionIndex]);
}
currentQuestionIndex++;
this.setState({ currentQuestionIndex });
}
}
renderPreviousButton() {
if (!this.props.renderPrevious) return;
let { currentQuestionIndex } = this.state;
return (
this.props.renderPrevious(() => {
currentQuestionIndex--;
this.setState({ currentQuestionIndex });
}, (currentQuestionIndex !== 0)
));
}
renderFinishOrNextButton() {
const { answers } = this.state;
const { survey } = this.props;
let { currentQuestionIndex } = this.state;
let enabled = false;
switch (survey[currentQuestionIndex].questionType) {
case 'MultipleSelectionGroup': enabled = this.validateMultipleSelectionSurveyAnswers(); break;
case 'Info': enabled = true; break;
default: enabled = Boolean(answers[currentQuestionIndex]) && (answers[currentQuestionIndex].value || answers[currentQuestionIndex].value === 0); break;
}
if (currentQuestionIndex === this.props.survey.length - 1) {
if (!this.props.renderFinished) return;
return (
this.props.renderFinished(() => {
if (this.props.onAnswerSubmitted && answers[currentQuestionIndex]) {
this.props.onAnswerSubmitted(answers[currentQuestionIndex]);
}
if (this.props.onSurveyFinished) {
// Remove empty answers, coming from info screens.
const filteredAnswers = answers.filter(n => n);
this.props.onSurveyFinished(filteredAnswers);
}
}, enabled));
}
if (!this.props.renderNext) return;
return (
this.props.renderNext(() => {
if (this.props.onAnswerSubmitted && answers[currentQuestionIndex]) {
this.props.onAnswerSubmitted(answers[currentQuestionIndex]);
}
currentQuestionIndex++;
this.setState({ currentQuestionIndex });
}, enabled)
);
}
renderNavButtons() {
const { navButtonContainerStyle } = this.props;
if (this.props.renderPrevious || this.props.renderNext || this.props.renderFinished) {
return (
<View style={navButtonContainerStyle}>
{this.renderPreviousButton && this.renderPreviousButton()}
{this.renderFinishOrNextButton && this.renderFinishOrNextButton()}
</View>
);
}
return;
}
validateSelectionGroupSettings(questionSettings, currentQuestionIndex) {
if (!questionSettings) return;
const { allowDeselect, defaultSelection, autoAdvance: autoAdvanceThisQuestion } = questionSettings;
if (allowDeselect !== undefined &&
typeof allowDeselect !== 'boolean') {
throw new Error(
`allowDeselect was not passed in as a boolean for question ${currentQuestionIndex}`
);
}
if (defaultSelection !== undefined && (this.props.autoAdvance || autoAdvanceThisQuestion)) {
throw new Error(
`Cannot set auto advance and a default selection for question ${currentQuestionIndex}`
);
}
if (autoAdvanceThisQuestion !== undefined &&
typeof autoAdvanceThisQuestion !== 'boolean') {
throw new Error(
`autoAdvance was not passed in as a boolean for ${currentQuestionIndex}`
);
}
}
renderSelectionGroup() {
const { survey, renderSelector, selectionGroupContainerStyle, containerStyle } = this.props;
const { currentQuestionIndex } = this.state;
const autoAdvanceThisQuestion = Boolean(this.props.survey[currentQuestionIndex].questionSettings && this.props.survey[currentQuestionIndex].questionSettings.autoAdvance);
this.validateSelectionGroupSettings(this.props.survey[currentQuestionIndex].questionSettings, currentQuestionIndex);
if (!this.selectionHandlers[currentQuestionIndex]) {
if (!this.props.survey[currentQuestionIndex].questionSettings) {
this.selectionHandlers[currentQuestionIndex] = new SelectionHandler({ maxMultiSelect: 1, allowDeselect: true });
} else {
const { allowDeselect, defaultSelection } = this.props.survey[currentQuestionIndex].questionSettings;
if (defaultSelection !== undefined && typeof defaultSelection !== 'number') {
throw new Error(
`Default Selection not specified as an index for question ${currentQuestionIndex}`
);
}
const options = {};
options.maxMultiSelect = 1;
options.allowDeselect = allowDeselect === undefined || allowDeselect === true;
options.defaultSelection = defaultSelection !== undefined ? defaultSelection : null;
this.selectionHandlers[currentQuestionIndex] = new SelectionHandler(options);
if (typeof options.defaultSelection === 'number') {
// Set timeout is used here to avoid updateAnswer's call to setState.
setTimeout(() => this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: survey[currentQuestionIndex].options[options.defaultSelection]
}), 0);
}
}
}
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(this.props.survey[currentQuestionIndex].questionText) : null}
<SelectionGroup
onPress={this.selectionHandlers[currentQuestionIndex].selectionHandler}
items={survey[currentQuestionIndex].options}
isSelected={this.selectionHandlers[currentQuestionIndex].isSelected}
renderContent={renderSelector}
containerStyle={selectionGroupContainerStyle}
onItemSelected={(item) => {
this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: item
});
(this.props.autoAdvance || autoAdvanceThisQuestion) && this.autoAdvance();
}}
onItemDeselected={() => {
this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: null
});
}}
/>
{this.renderNavButtons()}
</View>
);
}
renderMultipleSelectionGroup() {
const { survey, renderSelector, selectionGroupContainerStyle, containerStyle } = this.props;
const { currentQuestionIndex } = this.state;
const { allowDeselect, defaultSelection, autoAdvance: autoAdvanceThisQuestion } =
this.props.survey[currentQuestionIndex].questionSettings;
const multiSelectMax = Number(this.props.survey[currentQuestionIndex].questionSettings.maxMultiSelect);
if (multiSelectMax === 1) {
return this.renderSelectionGroup(); // Why declare multiple selectif only 1 item can be selected?
}
this.validateSelectionGroupSettings(this.props.survey[currentQuestionIndex].questionSettings);
if (!this.selectionHandlers[currentQuestionIndex]) {
if (this.props.survey[currentQuestionIndex].questionSettings.maxMultiSelect) {
if (defaultSelection !== undefined && !Array.isArray(defaultSelection)) {
throw new Error(
`Default Selection not specified as an array for multiple selection question ${currentQuestionIndex}`
);
}
const options = {};
options.maxMultiSelect = multiSelectMax;
options.allowDeselect = allowDeselect === undefined || allowDeselect === true;
options.defaultSelection = defaultSelection !== undefined ? defaultSelection : null;
this.selectionHandlers[currentQuestionIndex] = new SelectionHandler(options);
if (Array.isArray(options.defaultSelection)) {
// Set timeout is used here to avoid updateAnswer's call to setState.
setTimeout(() => this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: survey[currentQuestionIndex].options.filter((element, index) => options.defaultSelection.includes(index))
}), 0);
}
}
}
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(this.props.survey[currentQuestionIndex].questionText) : null}
<SelectionGroup
onPress={this.selectionHandlers[currentQuestionIndex].selectionHandler}
items={survey[currentQuestionIndex].options}
isSelected={this.selectionHandlers[currentQuestionIndex].isSelected}
getAllSelectedItemIndexes={this.selectionHandlers[currentQuestionIndex].getAllSelectedItemIndexes}
renderContent={renderSelector}
containerStyle={selectionGroupContainerStyle}
onItemSelected={(item, allSelectedItems) => {
this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: allSelectedItems
});
(autoAdvanceThisQuestion || this.props.autoAdvance) && this.autoAdvance();
}}
onItemDeselected={(item, allSelectedItems) => {
this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: allSelectedItems
});
}}
/>
{this.renderNavButtons()}
</View>
);
}
renderNumeric() {
const { survey, renderNumericInput, containerStyle } = this.props;
const currentQuestionIndex = this.state.currentQuestionIndex;
const answers = this.state.answers;
const { questionText, questionId, placeholderText = null, defaultValue = '' } = survey[currentQuestionIndex];
if (answers[currentQuestionIndex] === undefined && (defaultValue || defaultValue === 0) && Number.isInteger(parseInt(`${defaultValue}`, 10))) {
setTimeout(() => this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: defaultValue
}), 0);
}
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(questionText) : null}
{renderNumericInput(
(value) => {
const valInt = parseInt(value, 10);
if (Number.isInteger(valInt)) {
this.updateAnswer({
questionId,
value: valInt
});
} else if (value === '') {
this.updateAnswer({
questionId,
value: ''
});
}
},
answers[currentQuestionIndex] === undefined ? '' : answers[currentQuestionIndex].value,
placeholderText,
this.props.autoAdvance ? this.autoAdvance.bind(this) : null
)}
{this.renderNavButtons()}
</View>
);
}
renderTextInputElement() {
const { survey, renderTextInput, containerStyle } = this.props;
const currentQuestionIndex = this.state.currentQuestionIndex;
const answers = this.state.answers;
const { questionText, questionId, placeholderText = null, defaultValue } = survey[currentQuestionIndex];
if (answers[currentQuestionIndex] === undefined && defaultValue) {
setTimeout(() => this.updateAnswer({
questionId: survey[currentQuestionIndex].questionId,
value: defaultValue
}), 0);
}
return (<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(questionText) : null}
{renderTextInput((value) =>
this.updateAnswer({
questionId,
value
}),
answers[currentQuestionIndex] === undefined ? undefined : answers[currentQuestionIndex].value,
placeholderText,
this.props.autoAdvance ? this.autoAdvance.bind(this) : null
)}
{this.renderNavButtons()}
</View>
);
}
renderInfo() {
const currentQuestionIndex = this.state.currentQuestionIndex;
const { survey, renderInfo, containerStyle } = this.props;
const { questionText } = survey[currentQuestionIndex];
return (<View style={containerStyle}>
{renderInfo(questionText)}
{this.renderNavButtons()}
</View>
);
}
renderAddressPicker() {
const { survey, containerStyle, userPos } = this.props; // Get userPos from props
const currentQuestionIndex = this.state.currentQuestionIndex;
const currentQuestion = survey[currentQuestionIndex];
// console.log("userPosuserPos",userPos);
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(currentQuestion.questionText) : null}
<AddressPicker
onSelect={(selectedAddress) => this.updateAnswer({
questionId: currentQuestion.questionId,
value: {formattedAddress : selectedAddress.formatted_address,geometry : selectedAddress.geometry}
})}
latitude={userPos.latitude}
longitude={userPos.longitude}
/>
{this.renderNavButtons()}
</View>
);
}
renderConditionalSelectionWithText() {
const { survey, renderSelector, renderTextInput, containerStyle, selectionGroupContainerStyle } = this.props;
const { currentQuestionIndex, answers } = this.state;
const currentQuestion = survey[currentQuestionIndex];
// Ensure a SelectionHandler instance is initialized for the current question
if (!this.selectionHandlers[currentQuestionIndex]) {
this.selectionHandlers[currentQuestionIndex] = new SelectionHandler({
maxMultiSelect: 1,
allowDeselect: true
});
}
const isOtherSelected = answers[currentQuestionIndex]?.value.value === 'other';
const valeOtherSelected = isOtherSelected? answers[currentQuestionIndex]?.value:null;
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(currentQuestion.questionText) : null}
<SelectionGroup
onPress={this.selectionHandlers[currentQuestionIndex].selectionHandler}
items={currentQuestion.options.map(option => ({...option, key: option.value}))}
isSelected={this.selectionHandlers[currentQuestionIndex].isSelected}
renderContent={renderSelector}
containerStyle={selectionGroupContainerStyle}
onItemSelected={(item) => {
this.updateAnswer({
questionId: currentQuestion.questionId,
value: item,
});
}}
onItemDeselected={() => {
this.updateAnswer({
questionId: currentQuestion.questionId,
value: null
});
}}
/>
{isOtherSelected && renderTextInput(
(value) => {
this.updateAnswer({
questionId: currentQuestion.questionId,
value: valeOtherSelected,
additionalText: value
});
},
answers[currentQuestionIndex]?.additionalText,
"Please specify" // Placeholder text for the "Other" input field
)}
{this.renderNavButtons()}
</View>
);
}
renderMultiAddressPicker() {
const { survey, containerStyle } = this.props;
const currentQuestion = survey[this.state.currentQuestionIndex];
return (
<View style={containerStyle}>
{this.props.renderQuestionText ?
this.props.renderQuestionText(currentQuestion.questionText) : null}
<TennisCourtSearch onSelect={(selectedAddress) => {
this.updateAnswer({
questionId: currentQuestion.questionId,
value: selectedAddress
});
}} />
{this.renderNavButtons()}
</View>
);
}
render() {
const { survey } = this.props;
const currentQuestion = this.state.currentQuestionIndex;
switch (survey[currentQuestion].questionType) {
case 'SelectionGroup': return this.renderSelectionGroup();
case 'MultipleSelectionGroup': return this.renderMultipleSelectionGroup();
case 'TextInput': return this.renderTextInputElement();
case 'NumericInput': return this.renderNumeric();
case 'Info': return this.renderInfo();
case 'ConditionalSelectionWithText': return this.renderConditionalSelectionWithText();
case 'AddressPicker': return this.renderAddressPicker();
case 'MultiAddressPicker': return this.renderMultiAddressPicker(); // Added new case
default: return <View />;
}
}
}
const findAllQuestionsWithAnswers = async (userId) => {
try {
const questionsWithAnswers = await db('questions as q')
.distinctOn('q.id')
.select(
db.raw(`CAST(q.id AS VARCHAR) as "questionId"`),
'it.input_type_name as questionType',
'q.question_text as questionText',
db.raw(`
CASE
WHEN it.input_type_name = 'TextInput' THEN 'Enter your name'
WHEN it.input_type_name = 'NumericInput' THEN 'Enter your zip code'
ELSE NULL
END AS "placeholderText"
`),
db.raw(`q.question_subtext AS "subText"`),
db.raw(`
(SELECT JSON_AGG(
JSON_BUILD_OBJECT(
'optionId', oc.id,
'optionText', oc.option_choice_name,
'value', LOWER(REPLACE(oc.option_choice_name, ' ', '')),
'description', oc.description,
'sortOrder', oc.sort_order,
'questionOptionId', qo.id
)
) FROM question_options as qo
JOIN option_choices as oc ON qo.option_choice_id = oc.id
WHERE qo.question_id = q.id
GROUP BY qo.question_id)
AS options
`),
db.raw(`
CASE
WHEN it.input_type_name = 'MultipleSelectionGroup' THEN
JSON_BUILD_OBJECT(
'maxMultiSelect', 3,
'minMultiSelect', 1,
'allowDeselection',true,
'allowDeselect',true
)
ELSE NULL
END AS "questionSettings"
`),
'a.answer_text as answerText',
'a.answer_numeric as answerNumeric',
db.raw(`
FIRST_VALUE(a.metadata) OVER(PARTITION BY q.id ORDER BY a.id) AS "answerMetadata"
`),
'a.additional_text as additionalText'
)
.leftJoin('input_types as it', 'q.input_type_id', 'it.id')
.leftJoin('answers as a', function() {
this.on('a.question_id', '=', 'q.id').andOn('a.user_id', '=', db.raw('?', [userId]));
})
.where('q.status', 1)
.orderBy('q.id')
.groupBy('q.id', 'it.input_type_name', 'a.id');
return questionsWithAnswers;
} catch (error) {
console.error("Error fetching questions with answers:", error);
throw new Error(error);
}
};
const upsertAnswers = async (userId, answersJson) => {
try {
// Assuming `db` is your database connection object
for (const answer of answersJson) {
const metadata = JSON.stringify(answer); // Convert answer to JSON string for storage
let answerData = {
user_id: userId,
question_id: answer.questionId,
metadata: metadata,
};
if (answer.questionType === 'AddressPicker'|| answer.questionType === 'MultiAddressPicker') {
const addresses = answer.questionType === 'MultiAddressPicker' ? answer.value : [answer.value];
for (const addressInfo of addresses) {
const locationId = await insertOrUpdateAddress(userId, addressInfo);
// Create answerData for each address
const specificAnswerData = {
...answerData,
answer_text: addressInfo.formattedAddress,
metadata: JSON.stringify({ ...answer, locationId }),
};
// Insert the specificAnswerData into the answers table
await db('answers').insert(specificAnswerData);
}
} else if (answer.questionType === 'TextInput' || answer.questionType === 'NumericInput') {
answerData = {
...answerData,
answer_text: answer.questionType === 'TextInput' ? answer.value : null,
answer_numeric: answer.questionType === 'NumericInput' ? answer.value : null,
};
} else if (answer.questionType === 'SelectionGroup' || answer.questionType === 'MultipleSelectionGroup' || answer.questionType === 'ConditionalSelectionWithText') {
// First, remove existing answers for these question types to avoid stale data
await db('answers')
.where({
user_id: userId,
question_id: answer.questionId
})
.delete();
// Then, insert the new selections
const options = answer.value instanceof Array ? answer.value : [answer.value];
for (const option of options) {
const optionData = {
...answerData,
question_option_id: option.questionOptionId,
additional_text: answer ? answer.additionalText : null,
};
await db('answers').insert(optionData);
}
}
}
return { success: true };
} catch (error) {
console.error("Error upserting answers:", error);
throw error;
}
};
const findAllQuestion = (id, day, location_id) => {
const queryBuilder = db("questions as q")
.select(
db.raw(`CAST(q.id AS VARCHAR) as "questionId"`),
'it.input_type_name as questionType',
'q.question_text as questionText',
db.raw(`
CASE
WHEN it.input_type_name = 'TextInput' THEN 'Enter your name'
WHEN it.input_type_name = 'NumericInput' THEN 'Enter your zip code'
ELSE NULL
END AS "placeholderText"
`),
db.raw(`q.question_subtext AS "subText"`),
db.raw(`
CASE
WHEN it.input_type_name IN ('SelectionGroup', 'MultipleSelectionGroup','ConditionalSelectionWithText') THEN
JSON_AGG(
JSON_BUILD_OBJECT(
'optionId', oc.id,
'optionText', oc.option_choice_name,
'value', LOWER(REPLACE(oc.option_choice_name, ' ', '')),
'description', oc.description,
'sortOrder', oc.sort_order,
'questionOptionId',qo.id
) ORDER BY oc.sort_order
)
ELSE NULL
END AS options
`),
db.raw(`
CASE
WHEN it.input_type_name = 'MultipleSelectionGroup' THEN
JSON_BUILD_OBJECT(
'maxMultiSelect', 3,
'minMultiSelect', 1,
'allowDeselection',true,
'allowDeselect',true
)
ELSE NULL
END AS "questionSettings"
`)
)
.where("q.status", 1)
.join('input_types as it', 'q.input_type_id', 'it.id')
.leftJoin('question_options as qo', 'q.id', 'qo.question_id')
.leftJoin('option_choices as oc', 'qo.option_choice_id', 'oc.id')
.groupBy('q.id', 'it.input_type_name')
.orderBy('q.id');
// Additional filtering or adjustments can be added here, if needed
return queryBuilder;
};
const removeUserAnswers = async (userId) => {
await db('answers')
.where({
user_id: userId
})
.delete();
};
exports.up = async function(knex) {
return knex.schema
// Organizations Table
.createTable('organizations', table => {
table.increments('id');
table.string('organization_name', 80).notNullable();
})
// Survey Headers Table
.createTable('survey_headers', table => {
table.increments('id');
table.integer('organization_id').unsigned().references('id').inTable('organizations').notNullable();
table.string('survey_name', 80).notNullable();
table.text('instructions');
table.string('other_header_info', 255);
table.boolean('is_active').defaultTo(true);
table.enu('status', ['draft', 'published', 'archived'], { useNative: true, enumName: 'survey_status' }).defaultTo('draft');
})
// Input Types Table
.createTable('input_types', table => {
table.increments('id');
table.string('input_type_name', 80).notNullable();
})
// Option Groups Table
.createTable('option_groups', table => {
table.increments('id');
table.string('option_group_name', 45).notNullable();
})
// Survey Sections Table
.createTable('survey_sections', table => {
table.increments('id');
table.integer('survey_header_id').unsigned().references('id').inTable('survey_headers').notNullable();
table.string('section_name', 80).notNullable();
table.string('section_title', 45);
table.string('section_subheading', 45);
table.boolean('section_required').notNullable();
table.integer('sort_order');
table.integer('status').defaultTo(1);
table.timestamps(true, true);
})
// Questions Table
.createTable('questions', table => {
table.increments('id');
table.integer('survey_section_id').unsigned().references('id').inTable('survey_sections').notNullable();
table.integer('input_type_id').unsigned().references('id').inTable('input_types').notNullable();
table.string('question_subtext', 500);
table.string('question_text', 500).notNullable();
table.boolean('answer_required').notNullable();
table.integer('option_group_id').unsigned().references('id').inTable('option_groups');
table.boolean('allow_multiple_option_answers');
table.integer('sort_order');
table.integer('status').defaultTo(1);
table.timestamps(true, true);
})
// Option Choices Table
.createTable('option_choices', table => {
table.increments('id');
table.integer('option_group_id').unsigned().references('id').inTable('option_groups').notNullable();
table.string('option_choice_name', 45).notNullable();
table.text('description');
table.integer('sort_order');
table.integer('status').defaultTo(1);
table.timestamps(true, true);
})
// Question Options Table
.createTable('question_options', table => {
table.increments('id');
table.integer('question_id').unsigned().references('id').inTable('questions').notNullable();
table.integer('option_choice_id').unsigned().references('id').inTable('option_choices').notNullable();
})
// User Survey Sections Table
.createTable('user_survey_sections', table => {
table.increments('id');
table.integer('user_id').notNullable()
.references("id")
.inTable("users")
.onDelete("CASCADE")
.index(); // Assuming foreign key setup elsewhere or adjusted as needed
table.integer('survey_section_id').unsigned().references('id').inTable('survey_sections').notNullable();
table.timestamp('completed_on');
})
// Survey Comments Table
.createTable('survey_comments', table => {
table.increments('id');
table.integer('survey_header_id').unsigned().references('id').inTable('survey_headers').notNullable();
table.integer('user_id').notNullable()
.references("id")
.inTable("users")
.onDelete("CASCADE")
.index(); // Assuming foreign key setup elsewhere or adjusted as needed
table.text('comments');
})
// Unit of Measures Table
.createTable('unit_of_measures', table => {
table.increments('id');
table.string('unit_of_measures_name', 80).notNullable();
})
// Answers Table
.createTable('answers', table => {
table.increments('id');
table.integer('user_id').notNullable()
.references("id")
.inTable("users")
.onDelete("CASCADE")
.index(); // Assuming foreign key setup elsewhere or adjusted as needed
table.integer('question_option_id').unsigned().references('id').inTable('question_options');
table.integer('answer_numeric');
table.string('answer_text', 255);
table.boolean('answer_boolean');
table.integer('question_id').unsigned().references('id').inTable('questions');
table.integer('unit_of_measure_id').unsigned().references('id').inTable('unit_of_measures');
table.date('answer_date');
table.timestamp('answer_datetime');
table.string('additional_text', 255);
table.jsonb('metadata');
table.timestamps(true, true);
table.string('answer_file', 255);
});
};
exports.down = async function(knex) {
// Drop tables in reverse creation order
return knex.schema.dropTableIfExists('answers')
.dropTableIfExists('survey_comments')
.dropTableIfExists('user_survey_sections')
.dropTableIfExists('question_options')
.dropTableIfExists('option_choices')
.dropTableIfExists('questions')
.dropTableIfExists('survey_sections')
.dropTableIfExists('option_groups')
.dropTableIfExists('input_types')
.dropTableIfExists('survey_headers')
.dropTableIfExists('organizations')
.dropTableIfExists('unit_of_measures');
};
// const survey = [
// {
// questionType: 'Info',
// questionText: 'Welcome to Find Your Tennis Partner!',
// subText: 'Tap next to continue',
// },
// {
// questionType: 'TextInput',
// questionText: 'What is your name?',
// questionId: 'playerName',
// placeholderText: 'Enter your name',
// subText: 'Tell TextInput',
// },
// {
// questionType: 'NumericInput',
// questionText: 'Location',
// questionId: 'zipcode',
// placeholderText: 'Enter your zip code',
// subText: 'Please tell a bit more about yourself',
// },
// {
// questionType: 'SelectionGroup',
// questionText: 'Choose your level?',
// subText: 'Please tell a bit more about yourself',
// questionId: 'playingStyle',
// options : [
// {
// optionText: 'Beginner (NTRP 2.5)',
// value: 'beginner',
// description: 'Just getting started in the game'
// },
// {
// optionText: 'Advanced Beginner (NTRP 3.0)',
// value: 'advancedBeginner',
// description: 'I can rally but my strokes are not consistent yet'
// },
// {
// optionText: 'Intermediate (NTRP 3.5)',
// value: 'intermediate',
// description: 'I can hit with spin and direction most of the time'
// },
// {
// optionText: 'Advanced (NTRP 4.0)',
// value: 'advanced',
// description: 'I can consistently rally with spin, direction, and pace'
// },
// {
// optionText: 'Advanced Plus (NTRP 4.5)',
// value: 'advancedPlus',
// description: 'I have control over my shots and hit consistently with depth and pace'
// },
// {
// optionText: 'Expert (NTRP 5.0)',
// value: 'expert',
// description: 'I have control over my shots and hit consistently with depth and pace'
// },
// ],
// },
// {
// questionType: 'MultipleSelectionGroup',
// questionText: 'Select two or three of your favorite tennis players!',
// questionId: 'favoritePlayers',
// questionSettings: { maxMultiSelect: 3, minMultiSelect: 2 },
// subText: 'select Max 3 or min 2',
// options: [
// { optionText: 'Roger Federer', value: 'rogerFederer' },
// { optionText: 'Serena Williams', value: 'serenaWilliams' },
// { optionText: 'Novak Djokovic', value: 'novakDjokovic' },
// { optionText: 'Naomi Osaka', value: 'naomiOsaka' },
// { optionText: 'Rafael Nadal', value: 'rafaelNadal' },
// ],
// },
// {
// questionType: 'MultipleSelectionGroup',
// questionText: 'You are looking for...',
// questionId: 'lookingFor',
// subText: 'Please tell a bit more about yourself',
// questionSettings: { maxMultiSelect: 3, minMultiSelect: 2 },
// options: [
// { optionText: 'Fun / social', value: 'funSocial' },
// { optionText: 'Casual hitting', value: 'casualHitting' },
// { optionText: 'Friendly competition', value: 'friendlyCompetition' },
// { optionText: 'High level competition', value: 'highLevelCompetition' },
// // { optionText: 'Rafael Nadal', value: 'rafaelNadal' },
// ],
// },
// {
// questionType: 'Info',
// questionText: 'Thank you. Tap finish to see your results!',
// subText: 'Info more about yourself',
// },
// ];
const survey = [
{
"questionId": "1",
"questionType": "TextInput",
"questionText": "What is your name?",
"placeholderText": "Enter your name",
"subText": "Please tell a bit more about yourself",
"options": null,
"questionSettings": null
},
{
questionType: 'ConditionalSelectionWithText',
questionText: 'What is your gender?',
questionId: 'gender',
options: [
{ optionText: 'Male', value: 'Male' },
{ optionText: 'Female', value: 'Female' },
{ optionText: 'Other', value: 'Other' }
]
}
// {
// "questionId": "2",
// "questionType": "NumericInput",
// "questionText": "Location",
// "placeholderText": "Enter your zip code",
// "subText": "Please tell a bit more about yourself",
// "options": null,
// "questionSettings": null
// },
// {
// "questionId": "3",
// "questionType": "SelectionGroup",
// "questionText": "Choose your level?",
// "placeholderText": null,
// "subText": "Please tell a bit more about yourself",
// "options": [
// {
// "optionId": 1,
// "optionText": "Beginner (NTRP 2.5)",
// "value": "beginner(ntrp2.5)",
// "description": "Just getting started in the game",
// "sortOrder": 1
// },
// {
// "optionId": 2,
// "optionText": "Advanced Beginner (NTRP 3.0)",
// "value": "advancedbeginner(ntrp3.0)",
// "description": "I can rally but my strokes are not consistent yet",
// "sortOrder": 2
// },
// {
// "optionId": 3,
// "optionText": "Intermediate (NTRP 3.5)",
// "value": "intermediate(ntrp3.5)",
// "description": "I can hit with spin and direction most of the time",
// "sortOrder": 3
// },
// {
// "optionId": 4,
// "optionText": "Advanced (NTRP 4.0)",
// "value": "advanced(ntrp4.0)",
// "description": "I can consistently rally with spin, direction, and pace",
// "sortOrder": 4
// },
// {
// "optionId": 5,
// "optionText": "Advanced Plus (NTRP 4.5)",
// "value": "advancedplus(ntrp4.5)",
// "description": "I have control over my shots and hit consistently with depth and pace",
// "sortOrder": 5
// },
// {
// "optionId": 6,
// "optionText": "Expert (NTRP 5.0)",
// "value": "expert(ntrp5.0)",
// "description": "I have competitive experience and advanced skill levels",
// "sortOrder": 6
// }
// ],
// "questionSettings": null
// },
// {
// "questionId": "4",
// "questionType": "MultipleSelectionGroup",
// "questionText": "Select two or three of your favorite tennis players!",
// "placeholderText": null,
// "subText": "Please tell a bit more about yourself",
// "options": [
// {
// "optionId": 7,
// "optionText": "Roger Federer",
// "value": "rogerfederer",
// "description": "A legendary player known for his precision and grace",
// "sortOrder": 1
// },
// {
// "optionId": 8,
// "optionText": "Serena Williams",
// "value": "serenawilliams",
// "description": "Famed for her powerful serve and determination",
// "sortOrder": 2
// },
// {
// "optionId": 9,
// "optionText": "Novak Djokovic",
// "value": "novakdjokovic",
// "description": "Renowned for his exceptional agility and endurance",
// "sortOrder": 3
// },
// {
// "optionId": 10,
// "optionText": "Naomi Osaka",
// "value": "naomiosaka",
// "description": "Admired for her powerful gameplay and strategic acumen",
// "sortOrder": 4
// },
// {
// "optionId": 11,
// "optionText": "Rafael Nadal",
// "value": "rafaelnadal",
// "description": "Celebrated for his dominance on clay courts and fighting spirit",
// "sortOrder": 5
// }
// ],
// "questionSettings": {
// "maxMultiSelect": 3,
// "minMultiSelect": 2
// }
// },
// {
// "questionId": "5",
// "questionType": "MultipleSelectionGroup",
// "questionText": "You are looking for...",
// "placeholderText": null,
// "subText": "Please tell a bit more about yourself",
// "options": [
// {
// "optionId": 12,
// "optionText": "Fun / social",
// "value": "fun/social",
// "description": "Looking to enjoy the game and meet new people",
// "sortOrder": 1
// },
// {
// "optionId": 13,
// "optionText": "Casual hitting",
// "value": "casualhitting",
// "description": "Interested in playing tennis casually without competitive pressure",
// "sortOrder": 2
// },
// {
// "optionId": 14,
// "optionText": "Friendly competition",
// "value": "friendlycompetition",
// "description": "Seeking a bit of competition while having fun",
// "sortOrder": 3
// },
// {
// "optionId": 15,
// "optionText": "High level competition",
// "value": "highlevelcompetition",
// "description": "Aiming for serious, competitive matches",
// "sortOrder": 4
// }
// ],
// "questionSettings": {
// "maxMultiSelect": 3,
// "minMultiSelect": 2
// }
// }
];
const Answer = [{ "1": "Gffggf", "2": 565665, "3": { "optionId": 1, "optionText": "Beginner (NTRP 2.5)", "value": "beginner(ntrp2.5)", "description": "Just getting started in the game", "sortOrder": 1 }, "4": [{ "optionId": 7, "optionText": "Roger Federer", "value": "rogerfederer", "description": "A legendary player known for his precision and grace", "sortOrder": 1 }, { "optionId": 8, "optionText": "Serena Williams", "value": "serenawilliams", "description": "Famed for her powerful serve and determination", "sortOrder": 2 }], "5": [{ "optionId": 13, "optionText": "Casual hitting", "value": "casualhitting", "description": "Interested in playing tennis casually without competitive pressure", "sortOrder": 2 }, { "optionId": 15, "optionText": "High level competition", "value": "highlevelcompetition", "description": "Aiming for serious, competitive matches", "sortOrder": 4 }] }];
export { survey };
import React, { useState, useRef,useEffect } from 'react';
import { View, Text, TouchableOpacity, FlatList, Modal, StyleSheet, Dimensions } from 'react-native';
import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
import { GOOGLE_MAPS_AUTOCOMPLETE_API_KEY } from '../../constants';
const { width, height } = Dimensions.get('window');
const TennisCourtSearch = ({ onSelect, latitude, longitude }) => {
const searchRef = useRef();
const [selectedCourts, setSelectedCourts] = useState([]);
const [showModal, setShowModal] = useState(false);
useEffect(() => {
searchRef.current?.setAddressText('Search for tennis courts');
}, []);
useEffect(() => {
onSelect(selectedCourts); // Notify parent component whenever selectedCourts changes
}, [selectedCourts]);
const handleSelectCourt = (detail) => {
const newCourt = {
formattedAddress: detail.formatted_address,
geometry: detail.geometry
};
const index = selectedCourts.findIndex(court => court.geometry.location === newCourt.geometry.location);
if (index === -1) {
setSelectedCourts(prevCourts => [...prevCourts, newCourt]);
} else {
setSelectedCourts(prevCourts => prevCourts.filter(court => court.geometry.location !== newCourt.geometry.location));
}
};
const renderCourtItem = ({ item, index }) => (
<View style={styles.courtItem}>
<Text style={styles.courtText} ellipsizeMode='tail' numberOfLines={1}>{`${index + 1}. ${item.formattedAddress}`}</Text>
<TouchableOpacity onPress={() => handleSelectCourt(item)} style={styles.removeButton}>
<Text style={styles.removeButtonText}>Remove</Text>
</TouchableOpacity>
</View>
);
console.log("selectedCourts",JSON.stringify(selectedCourts));
return (
<View style={{ marginTop: 50 }} keyboardShouldPersistTaps="handled">
<GooglePlacesAutocomplete
ref={searchRef}
placeholder="Search for tennis courts"
minLength={2}
fetchDetails={true}
onPress={(data, details = null) => {
if (data && details) {
handleSelectCourt(details);
}
}}
query={{
key: GOOGLE_MAPS_AUTOCOMPLETE_API_KEY,
language: 'en',
location: `${latitude},${longitude}`,
radius: 10000,
type: 'establishment',
keyword: 'tennis court',
}}
// disableScroll={true}
nearbyPlacesAPI="GooglePlacesSearch"
debounce={300}
styles={{
textInputContainer: { width: '100%' },
textInput: { height: 38, color: '#5d5d5d', fontSize: 16 },
}}
/>
<TouchableOpacity onPress={() => setShowModal(true)} style={styles.button}>
<Text>Show Selected Courts ({selectedCourts.length})</Text>
</TouchableOpacity>
<Modal
visible={showModal}
animationType="slide"
onRequestClose={() => setShowModal(false)}
>
<View style={styles.modalView}>
<FlatList
data={selectedCourts}
renderItem={renderCourtItem}
keyExtractor={item => item.place_id}
ListEmptyComponent={<Text>No courts selected.</Text>}
/>
<TouchableOpacity onPress={() => setShowModal(false)} style={styles.button}>
<Text>Close</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
button: {
marginTop: 10,
backgroundColor: 'lightgrey',
padding: 10,
alignItems: 'center',
},
courtItem: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
courtText: {
fontSize: 16,
width:200,
},
removeButton: {
padding: 5,
backgroundColor: 'red',
borderRadius: 5,
},
removeButtonText: {
color: 'white',
},
modalView: {
width: width * 0.9,
height: height * 0.7,
alignSelf: 'center',
marginTop: height * 0.15,
backgroundColor: 'white',
borderRadius: 20,
padding: 20,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
});
export default TennisCourtSearch;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment