RyanHub – file viewer
filename: app/(tabs)/Track.jsx
branch: master
back to repo
import { View, StyleSheet, FlatList, TextInput, Text, ActivityIndicator, Animated } from 'react-native'
import React, { useEffect } from 'react'
import { useState } from 'react'
import FoodItem from '@/components/FoodItem'

import { AntDesign } from '@expo/vector-icons'

import { useSQLiteContext } from 'expo-sqlite';

import { SearchFood } from '../../functions/SearchFood'
import { useAppTheme, subscribeToTheme } from '../../hooks/colorScheme';

export default function Track({ navigation }) {
  const [colors, setColors] = useState(useAppTheme());

  const [searchResults, setSearchResults] = useState([]);
  const [noResults, setNoResults] = useState(false);
  const [search, setSearch] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [showEmptySearchPrompt, setShowEmptySearchPrompt] = useState(false);
  const fadeAnim = React.useRef(new Animated.Value(0)).current;

  const searchCustomFoods = async (searchTerm) => {
    try {
      const results = await database.getAllAsync("SELECT * FROM customfoods");
      const matchingResults = results.filter(item => 
        item.name.toLowerCase().trim().includes(searchTerm.toLowerCase().trim())
      );
      return matchingResults;
    } catch (error) {
      console.error("Error searching custom foods:", error);
      return [];
    }
  };

  // Updated performSearch function
  const performSearch = async (searchTerm) => {
    // Reset states
    setError(null);
    
    // Handle empty search
    if (!searchTerm?.trim()) {
      setSearchResults([]);
      setNoResults(false);
      setShowEmptySearchPrompt(true);
      // Trigger fade in
      fadeAnim.setValue(0);
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 300,
        useNativeDriver: true,
      }).start();
      return;
    }
    
    setShowEmptySearchPrompt(false);
    setIsLoading(true);

    try {
      const apiResults = await SearchFood(searchTerm);
      const customResults = await searchCustomFoods(searchTerm);
      
      const modifiedCustomResults = customResults.map(item => ({
        ...item,
        id: `custom_${item.id}`
      }));
      
      const combinedResults = [...modifiedCustomResults, ...apiResults];
      setSearchResults(combinedResults);
      setNoResults(combinedResults.length === 0);
    } catch (error) {
      // attempt search again
      try {
        const apiResults = await SearchFood(searchTerm);
        const customResults = await searchCustomFoods(searchTerm);

        const modifiedCustomResults = customResults.map(item => ({
          ...item,
          id: `custom_${item.id}`
        }));

        const combinedResults = [...modifiedCustomResults, ...apiResults];
        setSearchResults(combinedResults);
        setNoResults(combinedResults.length === 0);
      } catch (error) {
      console.error("second search failed:", error);
      }
      
      console.error("Search failed:", error);
      setError("Failed to load results. Please try again.");
      setSearchResults([]);
    } finally {
      setIsLoading(false);
    }
  };

  const ItemSeparator = () => (
    <View style={{ height: 5 }} />
  );

  const getItemLayout = (data, index) => ({
    length: 80, // Approximate height of each item
    offset: 80 * index,
    index,
  });

  const database = useSQLiteContext();

  useEffect(() => {
    const unsubscribe = subscribeToTheme(newColors => {
      setColors(newColors);
    });
    return () => unsubscribe();
  }, []);

return (
  <View style={[styles.container, {backgroundColor: colors.background}]}>
    <View style={[styles.card, {backgroundColor: colors.boxes}]}>
      <TextInput 
        value={search} 
        onChangeText={setSearch}
        onSubmitEditing={() => performSearch(search)}
        returnKeyType="search"
        placeholder="Search for Foods..." 
        placeholderTextColor={colors.text + '80'}
        style={[styles.input, {color: colors.text}]} 
      />
    </View>

    <View style={[styles.buttonContainer, {backgroundColor: colors.background}]}> 
      <View style={[styles.buttonCard, {backgroundColor: colors.boxes}]}>
        <AntDesign.Button 
          style={styles.button}
          name="search1" 
          activeOpacity={0.5}
          underlayColor={'transparent'}
          onPress={() => performSearch(search)}
          backgroundColor="transparent"
          color={colors.accent}>
          Search
        </AntDesign.Button>
      </View>

      {false && <View style={[styles.buttonCard, {backgroundColor: colors.boxes}]}>
        <AntDesign.Button 
          style={styles.button}
          name="scan1" 
          activeOpacity={0.5}
          underlayColor={'transparent'}
          onPress={() => navigation.navigate('Scan')}
          backgroundColor="transparent"
          color={colors.accent}>
          Scan
        </AntDesign.Button>
      </View>}
      
      <View style={[styles.buttonCard, {backgroundColor: colors.boxes}]}>
        <AntDesign.Button 
          style={styles.button}
          name="addfile" 
          activeOpacity={0.5}
          underlayColor={'transparent'}
          onPress={() => navigation.navigate('QuickAdd')}
          backgroundColor="transparent"
          color={colors.accent}>
          Quick Add
        </AntDesign.Button>
      </View>
    </View>

    {isLoading && (
      <View style={styles.centerContent}>
        <ActivityIndicator size="large" color={colors.accent} />
        <Text style={[styles.messageText, {color: colors.text}]}>
          Searching...
        </Text>
      </View>
    )}

    {error && (
      <View style={styles.centerContent}>
        <Text style={[styles.errorText, {color: colors.error || '#ff6b6b'}]}>
          {error}
        </Text>
      </View>
    )}

    {showEmptySearchPrompt && !isLoading && !error && (
      <Animated.Text style={[
        styles.noResults, 
        {
          color: colors.text,
          opacity: fadeAnim
        }
      ]}>
        Please enter a food name to search
      </Animated.Text>
    )}

    {noResults && !isLoading && !error && !showEmptySearchPrompt && (
      <Text style={[styles.noResults, {color: colors.text}]}>
        No results found...
      </Text>
    )}

    {!isLoading && !error && (
      <FlatList
        data={searchResults} // Using state-managed results
        keyExtractor={item => item.id ? item.id.toString() : Math.random().toString()}
        renderItem={({item}) => <FoodItem item={item} />}
        ItemSeparatorComponent={ItemSeparator}
        getItemLayout={getItemLayout}
        initialNumToRender={10}
        maxToRenderPerBatch={10}
        windowSize={5}
        showsVerticalScrollIndicator={false}
        removeClippedSubviews={true}
        contentContainerStyle={{paddingHorizontal: 5}}
      />
    )}
    </View>)
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 15,
    gap: 16,
  },
  card: {
    borderRadius: 20,
    padding: 5,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 5,
    marginTop: 65,
  },
  input: {
    padding: 10,
    fontSize: 16,
    fontWeight: '500',
  },
  button: {
    justifyContent: 'center',
    minWidth: "45%",
    height: 50,
    paddingHorizontal: 15,
  },
  buttonContainer: {
    flexDirection: 'row', 
    justifyContent: 'space-evenly',
    paddingHorizontal: 5,
  },
  buttonCard: {
    borderRadius: 20,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 5,
  },
  noResults: {
    fontSize: 16,
    marginTop: 50,
    textAlign: 'center',
  },
  centerContent: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  messageText: {
    fontSize: 16,
    marginTop: 10,
    textAlign: 'center',
  },
  errorText: {
    fontSize: 16,
    textAlign: 'center',
    marginBottom: 10,
  },
})