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.

  1. 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
  1. Initialize the Expo project by running:

expo init frontend

When prompted to choose a template, select the blank template.

  1. 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

Setting Up Navigation#

To enable navigation between different screens in your app, replace the code in your App.js file with the following:

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import TitleScreen from './screens/TitleScreen';
import OptionsScreen from './screens/OptionsScreen';
import ConversionScreen from './screens/ConversionScreen';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Title">
        <Stack.Screen name="Title" component={TitleScreen} options={{ headerShown: false }} />
        <Stack.Screen name="Options" component={OptionsScreen} options={{ headerShown: false }} />
        <Stack.Screen name="Conversion" component={ConversionScreen} options={{ headerShown: false }} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

What This Code Does#

  • NavigationContainer wraps the entire app and manages navigation state.

  • createNativeStackNavigator creates a stack-based navigation system, allowing users to move between screens.

  • We define three screens:

    • TitleScreen (the app’s welcome or home screen)

    • OptionsScreen (where users select unit categories)

    • ConversionScreen (where users input values and see conversion results)

  • The initial screen shown when the app starts is "Title".

  • We hide the default headers (headerShown: false) for a cleaner, custom UI look.


Next, you can create the screens folder and add these three screen components.

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