How to Build an App – Frontend#
Laying Out the Framework#
For the frontend, we chose React Native with Expo. This powerful combination enables us to create native mobile apps for both Android and iOS using JavaScript and React. Expo accelerates development with built-in tools for building, testing, and deploying apps quickly and efficiently.
We’ll explore how to set up the frontend, build the user interface, and connect it to our FastAPI backend in the following sections, creating a smooth and responsive user experience.
Seting Up the App#
To get started with the frontend, we’ll use Expo CLI to create a new React Native project.
Install Expo CLI globally (if you haven’t already) by running this command in your terminal, from the root of your project:
npm install -g expo-cli
Initialize the Expo project by running:
expo init frontend
When prompted to choose a template, select the blank template.
Navigate into the frontend folder:
cd frontend
This sets up your frontend project, ready for development. In the next steps, we’ll start adding screens, components, and API integration.
Installations#
To add navigation and essential UI components to your Expo frontend, run the following commands inside your frontend
folder:
npm install @react-navigation/native
npm install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack
Then, install required native dependencies with Expo’s install command:
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-masked-view/masked-view
For dropdown selectors, install the picker component:
npm install @react-native-picker/picker
Finally, install the linear gradient library for UI styling:
npx expo install expo-linear-gradient
Creating Screens Files#
To organize your frontend code, create a screens
folder inside your frontend
project directory with the following structure:
frontend/
├── App.js
└── screens/
├── TitleScreen.js
├── OptionsScreen.js
└── ConversionScreen.js
Each of these files will contain the React Native components for the corresponding screens in your app.
Creating the Title Screen#
The TitleScreen
serves as the welcome screen of our app. It features a beautiful gradient background, animated entrance effects, and a “Start Converting” button that navigates users to the next screen.
Code Overview#
// TitleScreen.js
import React, { useEffect, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
// Get device dimensions for responsive layout
const { width, height } = Dimensions.get('window');
export default function TitleScreen({ navigation }) {
// Animation references
const fadeAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(0.8)).current;
const slideAnim = useRef(new Animated.Value(50)).current;
const buttonScaleAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
// Animate elements in a staggered sequence
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
tension: 50,
friction: 7,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 800,
useNativeDriver: true,
}),
]),
]).start();
}, []);
// Button press animation and navigation
const handleButtonPress = () => {
Animated.sequence([
Animated.timing(buttonScaleAnim, {
toValue: 0.95,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(buttonScaleAnim, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
]).start(() => {
navigation.navigate('Options');
});
};
return (
<LinearGradient
colors={['#667eea', '#764ba2', '#f093fb']}
style={styles.container}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.content}>
{/* Decorative background circles */}
<View style={styles.circleContainer}>
<View style={[styles.circle, styles.circle1]} />
<View style={[styles.circle, styles.circle2]} />
<View style={[styles.circle, styles.circle3]} />
</View>
{/* Animated app title */}
<Animated.View
style={[
styles.titleContainer,
{
opacity: fadeAnim,
transform: [{ scale: scaleAnim }],
},
]}
>
<Text style={styles.title}>Convert!</Text>
<Text style={styles.subtitle}>Universal Unit Converter</Text>
</Animated.View>
{/* Feature icons and text */}
<Animated.View
style={[
styles.featuresContainer,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
<View style={styles.featureRow}>
<Text style={styles.featureText}>📏 Length</Text>
<Text style={styles.featureText}>⚖️ Weight</Text>
</View>
<View style={styles.featureRow}>
<Text style={styles.featureText}>🌡️ Temperature</Text>
<Text style={styles.featureText}>⏱️ Time</Text>
</View>
</Animated.View>
{/* Animated start button */}
<Animated.View
style={[
styles.buttonContainer,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }, { scale: buttonScaleAnim }],
},
]}
>
<TouchableOpacity
style={styles.startButton}
onPress={handleButtonPress}
activeOpacity={0.8}
>
<LinearGradient
colors={['#ff9a9e', '#fecfef', '#fecfef']}
style={styles.buttonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.buttonText}>Start Converting</Text>
<Text style={styles.buttonIcon}>→</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
</View>
</LinearGradient>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
circleContainer: {
position: 'absolute',
width: width,
height: height,
},
circle: {
position: 'absolute',
borderRadius: 999,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
circle1: {
width: 200,
height: 200,
top: height * 0.1,
left: -50,
},
circle2: {
width: 150,
height: 150,
top: height * 0.7,
right: -30,
},
circle3: {
width: 100,
height: 100,
top: height * 0.3,
right: width * 0.2,
},
titleContainer: {
alignItems: 'center',
marginBottom: 40,
},
title: {
fontSize: 64,
fontWeight: '900',
color: '#ffffff',
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 2, height: 2 },
textShadowRadius: 4,
letterSpacing: 2,
},
subtitle: {
fontSize: 18,
color: 'rgba(255, 255, 255, 0.9)',
marginTop: 8,
fontWeight: '300',
letterSpacing: 1,
},
featuresContainer: {
marginBottom: 50,
},
featureRow: {
flexDirection: 'row',
justifyContent: 'space-between',
width: width * 0.7,
marginVertical: 8,
},
featureText: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: '500',
flex: 1,
textAlign: 'center',
},
buttonContainer: {
marginBottom: 40,
},
startButton: {
borderRadius: 30,
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
buttonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 18,
paddingHorizontal: 40,
borderRadius: 30,
},
buttonText: {
fontSize: 20,
fontWeight: '700',
color: '#333',
marginRight: 8,
letterSpacing: 0.5,
},
buttonIcon: {
fontSize: 20,
color: '#333',
fontWeight: 'bold',
},
});
What This Screen Does#
Uses Expo’s LinearGradient for a vibrant background.
Animates the title and button with React Native’s Animated API for smooth fade, scale, and slide effects.
Displays feature icons with text for the unit categories.
The Start Converting button scales on press and navigates to the OptionsScreen.
Creating the Options Screen#
The OptionsScreen
allows users to select which category of units they want to convert: Length, Weight, Cooking, or Temperature. It continues the app’s clean, animated style with buttons that navigate to the conversion screen.
Code Overview#
// OptionsScreen.js
import React, { useEffect, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
const { width, height } = Dimensions.get('window');
const conversionOptions = [
{ type: 'length', title: 'Length', icon: '📏', description: 'Meters, Feet, Inches...' },
{ type: 'weight', title: 'Weight', icon: '⚖️', description: 'Kilograms, Pounds, Ounces...' },
{ type: 'cooking', title: 'Cooking', icon: '🍳', description: 'Cups, Tablespoons, Milliliters...' },
{ type: 'temperature', title: 'Temperature', icon: '🌡️', description: 'Celsius, Fahrenheit, Kelvin...' },
];
export default function OptionsScreen({ navigation }) {
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(30)).current;
const buttonAnims = useRef(conversionOptions.map(() => new Animated.Value(0))).current;
useEffect(() => {
// Animate screen entrance
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 800,
useNativeDriver: true,
}),
]).start();
// Stagger button animations for smooth entrance
const buttonAnimations = buttonAnims.map((anim, index) =>
Animated.timing(anim, {
toValue: 1,
duration: 400,
delay: index * 100,
useNativeDriver: true,
})
);
Animated.stagger(100, buttonAnimations).start();
}, []);
const handleOptionPress = (type) => {
navigation.navigate('Conversion', { type });
};
const renderOption = (option, index) => {
const buttonScale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.timing(buttonScale, {
toValue: 0.95,
duration: 100,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.timing(buttonScale, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}).start();
};
return (
<Animated.View
key={option.type}
style={[
styles.optionWrapper,
{
opacity: buttonAnims[index],
transform: [
{
translateY: buttonAnims[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0],
}),
},
{ scale: buttonScale },
],
},
]}
>
<TouchableOpacity
style={styles.optionButton}
onPress={() => handleOptionPress(option.type)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
activeOpacity={0.8}
>
<LinearGradient
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
style={styles.optionGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.optionContent}>
<Text style={styles.optionIcon}>{option.icon}</Text>
<View style={styles.optionTextContainer}>
<Text style={styles.optionTitle}>{option.title}</Text>
<Text style={styles.optionDescription}>{option.description}</Text>
</View>
<Text style={styles.optionArrow}>→</Text>
</View>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
);
};
return (
<LinearGradient
colors={['#667eea', '#764ba2', '#f093fb']}
style={styles.container}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.circleContainer}>
<View style={[styles.circle, styles.circle1]} />
<View style={[styles.circle, styles.circle2]} />
<View style={[styles.circle, styles.circle3]} />
</View>
<View style={styles.content}>
{/* Animated header */}
<Animated.View
style={[
styles.headerContainer,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
<Text style={styles.title}>Select Conversion Type</Text>
<Text style={styles.subtitle}>Choose what you'd like to convert</Text>
</Animated.View>
{/* Conversion options */}
<View style={styles.optionsContainer}>
{conversionOptions.map((option, index) => renderOption(option, index))}
</View>
{/* Back button */}
<Animated.View style={[styles.backButtonContainer, { opacity: fadeAnim }]}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Text style={styles.backButtonText}>← Back</Text>
</TouchableOpacity>
</Animated.View>
</View>
</LinearGradient>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 100,
paddingBottom: 40,
},
circleContainer: {
position: 'absolute',
width: width,
height: height,
},
circle: {
position: 'absolute',
borderRadius: 999,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
circle1: {
width: 180,
height: 180,
top: height * 0.05,
right: -40,
},
circle2: {
width: 120,
height: 120,
bottom: height * 0.15,
left: -30,
},
circle3: {
width: 80,
height: 80,
top: height * 0.4,
left: width * 0.1,
},
headerContainer: {
paddingTop: 40,
alignItems: 'center',
marginBottom: 30,
},
title: {
fontSize: 32,
fontWeight: '800',
color: '#ffffff',
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 3,
letterSpacing: 0.5,
},
subtitle: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center',
marginTop: 8,
fontWeight: '300',
},
optionsContainer: {
flex: 1,
justifyContent: 'center',
},
optionWrapper: {
marginBottom: 16,
},
optionButton: {
borderRadius: 20,
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
},
optionGradient: {
borderRadius: 20,
padding: 20,
},
optionContent: {
flexDirection: 'row',
alignItems: 'center',
},
optionIcon: {
fontSize: 32,
marginRight: 16,
},
optionTextContainer: {
flex: 1,
},
optionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#333',
marginBottom: 4,
},
optionDescription: {
fontSize: 14,
color: '#666',
fontWeight: '400',
},
optionArrow: {
fontSize: 24,
color: '#333',
fontWeight: 'bold',
},
backButtonContainer: {
alignItems: 'center',
marginTop: 20,
},
backButton: {
paddingVertical: 12,
paddingHorizontal: 24,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 25,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
backButtonText: {
fontSize: 16,
color: '#ffffff',
fontWeight: '600',
},
});
What This Screen Does#
Provides a gradient background consistent with the app’s design.
Uses React Native’s Animated API to fade in and slide up the header and buttons smoothly.
Lists four conversion categories with icons, titles, and descriptions.
Each category is an animated button that scales on press, giving tactile feedback.
Pressing a category navigates to the Conversion Screen, passing the selected type.
Includes a Back button to return to the previous screen.
Creating the Conversion Screen#
The ConversionScreen
allows users to input a value, select units to convert from and to, and then perform the conversion. It features a beautiful gradient background, animated entrance effects, smooth unit swapping, and a responsive convert button that triggers a backend API call.
Code Overview#
// ConversionScreen.js
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Animated, Dimensions, Alert } from 'react-native';
import { Picker } from '@react-native-picker/picker';
import { LinearGradient } from 'expo-linear-gradient';
const { width, height } = Dimensions.get('window');
const typeIcons = {
length: '📏',
weight: '⚖️',
cooking: '🍳',
temperature: '🌡️',
};
export default function ConversionScreen({ route, navigation }) {
const { type } = route.params;
const [value, setValue] = useState('');
const [fromUnit, setFromUnit] = useState('');
const [toUnit, setToUnit] = useState('');
const [result, setResult] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// Animation references
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(30)).current;
const resultAnim = useRef(new Animated.Value(0)).current;
const units = {
length: ['inches', 'cm', 'feet', 'meters'],
weight: ['grams', 'kg', 'pounds', 'ounces'],
cooking: ['tsp', 'tbsp', 'cups', 'ml'],
temperature: ['Celsius', 'Fahrenheit', 'Kelvin'],
};
useEffect(() => {
// Animate screen entrance
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
Animated.timing(slideAnim, { toValue: 0, duration: 800, useNativeDriver: true }),
]).start();
// Initialize unit selectors
if (units[type].length > 0) {
setFromUnit(units[type][0]);
setToUnit(units[type][1] || units[type][0]);
}
}, []);
useEffect(() => {
// Animate result display when available
if (result !== null) {
Animated.spring(resultAnim, { toValue: 1, tension: 50, friction: 7, useNativeDriver: true }).start();
}
}, [result]);
const API_URL = 'YOU_WILL_CHANGE_THIS';
const handleConvert = async () => {
if (!value || !fromUnit || !toUnit) return;
setIsLoading(true);
try {
const response = await fetch(`${API_URL}/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
value: parseFloat(value),
from_unit: fromUnit,
to_unit: toUnit,
conversion_type: type,
}),
});
if (!response.ok) throw new Error('Conversion failed');
const data = await response.json();
setResult(data.result);
} catch {
Alert.alert('Error', 'Conversion failed. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleSwapUnits = () => {
setFromUnit(toUnit);
setToUnit(fromUnit);
setResult(null);
resultAnim.setValue(0);
};
return (
<LinearGradient colors={['#667eea', '#764ba2', '#f093fb']} style={styles.container} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }}>
{/* Decorative background circles */}
<View style={styles.circleContainer}>
<View style={[styles.circle, styles.circle1]} />
<View style={[styles.circle, styles.circle2]} />
<View style={[styles.circle, styles.circle3]} />
</View>
<View style={styles.content}>
{/* Header */}
<Animated.View style={[styles.headerContainer, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}>
<Text style={styles.typeIcon}>{typeIcons[type]}</Text>
<Text style={styles.header}>Convert {type} units</Text>
<Text style={styles.subtitle}>Enter a value and select units</Text>
</Animated.View>
{/* Input Section */}
<Animated.View style={[styles.inputSection, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}>
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Enter Value</Text>
<TextInput
style={styles.input}
placeholder="0"
placeholderTextColor="rgba(255, 255, 255, 0.6)"
keyboardType="numeric"
value={value}
onChangeText={setValue}
/>
</View>
</Animated.View>
{/* Unit Selectors */}
<Animated.View style={[styles.unitsSection, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}>
<View style={styles.unitRow}>
<View style={styles.pickerContainer}>
<Text style={styles.pickerLabel}>From</Text>
<View style={styles.pickerWrapper}>
<Picker selectedValue={fromUnit} onValueChange={setFromUnit} style={styles.picker} dropdownIconColor="#333">
{units[type].map(unit => <Picker.Item label={unit} value={unit} key={unit} color="#333" />)}
</Picker>
</View>
</View>
<TouchableOpacity style={styles.swapButton} onPress={handleSwapUnits}>
<Text style={styles.swapIcon}>⇄</Text>
</TouchableOpacity>
<View style={styles.pickerContainer}>
<Text style={styles.pickerLabel}>To</Text>
<View style={styles.pickerWrapper}>
<Picker selectedValue={toUnit} onValueChange={setToUnit} style={styles.picker} dropdownIconColor="#333">
{units[type].map(unit => <Picker.Item label={unit} value={unit} key={unit} color="#333" />)}
</Picker>
</View>
</View>
</View>
</Animated.View>
{/* Convert Button */}
<Animated.View style={[styles.buttonContainer, { opacity: fadeAnim }]}>
<TouchableOpacity
style={[styles.convertButton, !(value && fromUnit && toUnit) && styles.convertButtonDisabled]}
onPress={handleConvert}
disabled={!(value && fromUnit && toUnit) || isLoading}
activeOpacity={0.8}
>
<LinearGradient
colors={value && fromUnit && toUnit ? ['#ff9a9e', '#fecfef'] : ['#ccc', '#999']}
style={styles.buttonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.buttonText}>{isLoading ? 'Converting...' : 'Convert'}</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
{/* Result Display */}
{result !== null && (
<Animated.View
style={[
styles.resultContainer,
{
opacity: resultAnim,
transform: [
{
scale: resultAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 1],
}),
},
],
},
]}
>
<LinearGradient
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
style={styles.resultGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={styles.resultLabel}>Result</Text>
<Text style={styles.resultValue}>{result}</Text>
<Text style={styles.resultUnit}>{toUnit}</Text>
</LinearGradient>
</Animated.View>
)}
{/* Back Button */}
<Animated.View style={[styles.backButtonContainer, { opacity: fadeAnim }]}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Text style={styles.backButtonText}>← Back to Options</Text>
</TouchableOpacity>
</Animated.View>
</View>
</LinearGradient>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 60,
paddingBottom: 40,
},
circleContainer: {
position: 'absolute',
width: width,
height: height,
},
circle: {
position: 'absolute',
borderRadius: 999,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
circle1: {
width: 160,
height: 160,
top: height * 0.1,
left: -40,
},
circle2: {
width: 100,
height: 100,
bottom: height * 0.2,
right: -20,
},
circle3: {
width: 120,
height: 120,
top: height * 0.5,
right: width * 0.15,
},
headerContainer: {
alignItems: 'center',
marginBottom: 40,
},
typeIcon: {
fontSize: 48,
marginBottom: 12,
},
header: {
fontSize: 28,
fontWeight: '800',
color: '#ffffff',
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 3,
letterSpacing: 0.5,
textTransform: 'capitalize',
},
subtitle: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center',
marginTop: 8,
fontWeight: '300',
},
inputSection: {
marginBottom: 30,
},
inputContainer: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
borderRadius: 16,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
inputLabel: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: 8,
fontWeight: '600',
},
input: {
fontSize: 24,
color: '#ffffff',
fontWeight: '700',
textAlign: 'center',
paddingVertical: 8,
},
unitsSection: {
marginBottom: 30,
},
unitRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
pickerContainer: {
flex: 1,
},
pickerLabel: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: 8,
fontWeight: '600',
textAlign: 'center',
},
pickerWrapper: {
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderRadius: 12,
overflow: 'hidden',
},
picker: {
height: 60,
},
swapButton: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 25,
width: 50,
height: 50,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 10,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
swapIcon: {
fontSize: 20,
color: '#ffffff',
fontWeight: 'bold',
},
buttonContainer: {
marginBottom: 30,
},
convertButton: {
borderRadius: 16,
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
},
convertButtonDisabled: {
opacity: 0.6,
},
buttonGradient: {
paddingVertical: 18,
paddingHorizontal: 40,
borderRadius: 16,
alignItems: 'center',
},
buttonText: {
fontSize: 18,
fontWeight: '700',
color: '#333',
letterSpacing: 0.5,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
resultContainer: {
marginBottom: 30,
},
resultGradient: {
borderRadius: 16,
padding: 24,
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
resultLabel: {
fontSize: 16,
color: '#666',
fontWeight: '600',
marginBottom: 8,
},
resultValue: {
fontSize: 36,
fontWeight: '800',
color: '#333',
marginBottom: 4,
},
resultUnit: {
fontSize: 18,
color: '#666',
fontWeight: '500',
},
backButtonContainer: {
alignItems: 'center',
marginTop: 'auto',
},
backButton: {
paddingVertical: 12,
paddingHorizontal: 24,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 25,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
backButtonText: {
fontSize: 16,
color: '#ffffff',
fontWeight: '600',
},
});
What This Screen Does#
Uses Expo’s LinearGradient for a smooth, colorful background.
Animates the header, inputs, buttons, and result with React Native’s Animated API for a polished experience.
Lets users input numeric values and pick units from dropdown selectors.
Allows swapping the “From” and “To” units with a tap.
Sends conversion requests to a backend API and shows the result with animation.
Includes a back button to return to the Options screen.
Next Steps: Deploying the App#
The next step is to deploy the app so it can be accessed beyond your local development environment.
Start by using ngrok to expose your local backend to the internet temporarily:
Start your FastAPI backend locally.
Use ngrok to generate a public URL that tunnels to your backend.
Update your frontend to use this temporary ngrok URL to send conversion requests.
Once you’re ready for a more permanent solution, deploy your backend to Render:
Push your FastAPI backend code to a public GitHub repository.
Create a new Web Service on Render, linking it to your GitHub repo.
After deployment, copy the Render service URL and update the frontend to use this URL instead of ngrok.
This will make your app fully functional and accessible from anywhere, completing the backend deployment process.
Acknowledgements#
By Meara Cox, Summer Internship, June 2025