Header Ads Widget

Ticker

6/recent/ticker-posts

VertiGrow

|--app

|---_layout.tsx

|---ArticleListScreen.tsx

|---CompalyListScreen.tsx

|---CropListScreen.tsx

|---HomeScreen.tsx

|---index.tsx

|---LoginScreen.tsx

|---SignupScreen.tsx

|--utils

|---axios.ts

|---storage.ts










 |--app

|---article/

|----[slug].tsx

import React, { useState, useEffect } from 'react';

import {

  View,

  Text,

  StyleSheet,

  ScrollView,

  Image,

  ActivityIndicator,

  TouchableOpacity

} from 'react-native';

import { useLocalSearchParams } from 'expo-router';

import axios from '../../utils/axios';

import Ionicons from 'react-native-vector-icons/Ionicons';


interface ArticleImage {

  id: number;

  url: string;

  formats?: {

    thumbnail?: { url: string };

    small?: { url: string };

    medium?: { url: string };

  };

}


interface Article {

  id: number;

  title: string;

  slug: string;

  publishedBy: string;

  publishedDate: string;

  mainImage?: ArticleImage;

  content?: Array<{

    __component: string;

    id: number;

    heading?: string;

    paragraph?: any[];

    image?: {

      data?: ArticleImage;

    };

  }>;

}


const ArticleDetailScreen = () => {

  const { slug } = useLocalSearchParams();

  const [article, setArticle] = useState<Article | null>(null);

  const [loading, setLoading] = useState(true);

  const [error, setError] = useState<string | null>(null);


  useEffect(() => {

    const fetchArticle = async () => {

      try {

        setLoading(true);

        setError(null);

        

        // Try with simplified population first

        const response = await axios.get('/articles', {

          params: {

            filters: { slug: { $eq: slug } },

            populate: ['mainImage', 'content.image']

          }

        });


        console.log('API Response:', response.data);


        if (!response.data?.data?.[0]) {

          throw new Error('Article not found');

        }


        // Transform the API response

        const apiArticle = response.data.data[0];

        const transformedArticle: Article = {

          id: apiArticle.id,

          title: apiArticle.title,

          slug: apiArticle.slug,

          publishedBy: apiArticle.publishedBy,

          publishedDate: apiArticle.publishedDate,

          mainImage: apiArticle.mainImage,

          content: apiArticle.content?.map(item => ({

            ...item,

            image: item.image ? { data: item.image } : undefined

          }))

        };


        setArticle(transformedArticle);

      } catch (err) {

        console.error('Error fetching article:', err);

        setError(err.response?.data?.error?.message || 

                err.message || 

                'Failed to load article. Please try again.');

      } finally {

        setLoading(false);

      }

    };


    fetchArticle();

  }, [slug]);


  const getImageUrl = (imageData?: ArticleImage) => {

    if (!imageData?.url) return null;

    const url = imageData.formats?.medium?.url || 

               imageData.formats?.small?.url || 

               imageData.url;

    return url.startsWith('http') ? url : `${axios.defaults.baseURL?.replace('/api', '')}${url}`;

  };


  const renderContent = () => {

    if (!article?.content) return null;


    return article.content.map((component, index) => {

      switch (component.__component) {

        case 'heading.heading':

          return (

            <Text key={`heading-${index}`} style={styles.heading}>

              {component.heading}

            </Text>

          );


        case 'paragraph.paragraph':

          return (

            <View key={`paragraph-${index}`} style={styles.paragraph}>

              {component.paragraph?.map((para, paraIndex) => {

                if (para.type === 'paragraph') {

                  return (

                    <Text key={`para-${paraIndex}`} style={styles.paragraphText}>

                      {para.children?.[0]?.text}

                    </Text>

                  );

                } else if (para.type === 'list') {

                  return (

                    <View key={`list-${paraIndex}`} style={styles.listContainer}>

                      {para.children?.map((item, itemIndex) => (

                        <View key={`item-${itemIndex}`} style={styles.listItem}>

                          <Text style={styles.bullet}>•</Text>

                          <Text style={styles.listText}>

                            {item.children?.[0]?.text}

                          </Text>

                        </View>

                      ))}

                    </View>

                  );

                }

                return null;

              })}

            </View>

          );


        case 'image.image':

          const imageUrl = getImageUrl(component.image?.data);

          return imageUrl ? (

            <Image

              key={`image-${index}`}

              source={{ uri: imageUrl }}

              style={styles.contentImage}

              resizeMode="contain"

              defaultSource={require('../../assets/placeholder-image.png')}

            />

          ) : null;


        default:

          return null;

      }

    });

  };


  if (loading) {

    return (

      <View style={styles.centerContainer}>

        <ActivityIndicator size="large" color="#2E7D32" />

      </View>

    );

  }


  if (error) {

    return (

      <View style={styles.centerContainer}>

        <Ionicons name="warning-outline" size={40} color="#dc3545" />

        <Text style={styles.errorText}>{error}</Text>

        <TouchableOpacity 

          style={styles.retryButton}

          onPress={() => window.location.reload()}

        >

          <Text style={styles.retryButtonText}>Try Again</Text>

        </TouchableOpacity>

      </View>

    );

  }


  if (!article) {

    return (

      <View style={styles.centerContainer}>

        <Ionicons name="document-text-outline" size={50} color="#6c757d" />

        <Text style={styles.emptyText}>Article not found</Text>

      </View>

    );

  }


  const mainImageUrl = getImageUrl(article.mainImage);


  return (

    <ScrollView contentContainerStyle={styles.container}>

      {mainImageUrl ? (

        <Image

          source={{ uri: mainImageUrl }}

          style={styles.mainImage}

          resizeMode="cover"

          defaultSource={require('../../assets/placeholder-image.png')}

        />

      ) : (

        <Image

          source={require('../../assets/placeholder-image.png')}

          style={styles.mainImage}

          resizeMode="cover"

        />

      )}

      

      <View style={styles.contentContainer}>

        <Text style={styles.title}>{article.title}</Text>

        

        <View style={styles.metaContainer}>

          <Text style={styles.author}>{article.publishedBy}</Text>

          <Text style={styles.date}>

            {new Date(article.publishedDate).toLocaleDateString('en-US', {

              year: 'numeric',

              month: 'long',

              day: 'numeric'

            })}

          </Text>

        </View>


        {renderContent()}

      </View>

    </ScrollView>

  );

};


const styles = StyleSheet.create({

  container: {

    paddingBottom: 40,

    backgroundColor: 'white',

  },

  centerContainer: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

    padding: 20,

  },

  errorText: {

    color: '#dc3545',

    fontSize: 16,

    marginVertical: 16,

    textAlign: 'center',

  },

  retryButton: {

    backgroundColor: '#2E7D32',

    paddingHorizontal: 24,

    paddingVertical: 12,

    borderRadius: 6,

  },

  retryButtonText: {

    color: 'white',

    fontWeight: '600',

  },

  emptyText: {

    fontSize: 18,

    color: '#6c757d',

    marginTop: 16,

  },

  mainImage: {

    width: '100%',

    height: 250,

    backgroundColor: '#f5f5f5',

  },

  contentContainer: {

    padding: 20,

  },

  title: {

    fontSize: 28,

    fontWeight: 'bold',

    marginBottom: 16,

    color: '#212529',

    lineHeight: 34,

  },

  metaContainer: {

    flexDirection: 'row',

    justifyContent: 'space-between',

    marginBottom: 24,

    borderBottomWidth: 1,

    borderBottomColor: '#e9ecef',

    paddingBottom: 16,

  },

  author: {

    fontSize: 14,

    color: '#495057',

    fontWeight: '600',

  },

  date: {

    fontSize: 14,

    color: '#6c757d',

  },

  heading: {

    fontSize: 22,

    fontWeight: 'bold',

    marginVertical: 16,

    color: '#2E7D32',

  },

  paragraph: {

    marginBottom: 16,

  },

  paragraphText: {

    fontSize: 16,

    lineHeight: 24,

    color: '#212529',

  },

  listContainer: {

    marginLeft: 16,

    marginBottom: 16,

  },

  listItem: {

    flexDirection: 'row',

    marginBottom: 8,

  },

  bullet: {

    marginRight: 8,

    color: '#2E7D32',

  },

  listText: {

    fontSize: 16,

    lineHeight: 24,

    color: '#212529',

    flex: 1,

  },

  contentImage: {

    width: '100%',

    height: 200,

    marginVertical: 16,

    borderRadius: 8,

    backgroundColor: '#f5f5f5',

  },

});


export default ArticleDetailScreen;

|---_layout.tsx

import { DefaultTheme, DarkTheme, ThemeProvider } from '@react-navigation/native';

import { Stack } from 'expo-router';

import { StatusBar } from 'expo-status-bar';

import { useEffect, useState } from 'react';

import Storage from '../utils/storage';  // Import Storage

import { useRouter } from 'expo-router';

import { Platform } from 'react-native';


export default function RootLayout() {

  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);

  const [theme, setTheme] = useState('light'); // Default theme

  const router = useRouter();


  // Function to check for JWT in localStorage or AsyncStorage

  const checkAuthentication = async () => {

    const jwt = await Storage.getItem('jwt');

    if (jwt) {

      setIsAuthenticated(true);

      return '/HomeScreen'; // Go to home screen if JWT exists

    } else {

      setIsAuthenticated(false);

      return '/LoginScreen'; // Redirect to login if no JWT

    }

  };


  // Check theme based on platform

  useEffect(() => {

    if (Platform.OS === 'web') {

      // For web, use window.matchMedia to get the theme

      const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');

      setTheme(darkModeQuery.matches ? 'dark' : 'light');


      // Listen for changes in theme preference

      darkModeQuery.addEventListener('change', (e) => {

        setTheme(e.matches ? 'dark' : 'light');

      });

    } else {

      // For React Native, use useColorScheme

      import('react-native').then(({ useColorScheme }) => {

        const colorScheme = useColorScheme();

        setTheme(colorScheme === 'dark' ? 'dark' : 'light');

      });

    }


    checkAuthentication();

  }, [router]);


  // Select theme

  const currentTheme = theme === 'dark' ? DarkTheme : DefaultTheme;


  return (

    <ThemeProvider value={currentTheme}>

      <Stack>

        {isAuthenticated ? (

          <Stack.Screen name="HomeScreen" options={{ headerShown: false }} />

        ) : (

          <Stack.Screen name="LoginScreen" options={{ headerShown: false }} />

        )}

      </Stack>

      <StatusBar style="auto" />

    </ThemeProvider>

  );

}


|---ArticleListScreen.tsx

import React, { useEffect, useState } from 'react';

import { 

  View, 

  Text, 

  StyleSheet, 

  FlatList, 

  Image, 

  TouchableOpacity, 

  ActivityIndicator,

  RefreshControl,

  Platform

} from 'react-native';

import { Link } from 'expo-router';

import axios from '../utils/axios';

import Ionicons from 'react-native-vector-icons/Ionicons';


interface ArticleImage {

  id: number;

  attributes: {

    url: string;

    formats?: {

      thumbnail?: {

        url: string;

      };

      small?: {

        url: string;

      };

      medium?: {

        url: string;

      };

    };

  };

}


interface Article {

    id: number;

    title: string;

    slug: string;

    publishedBy: string;

    publishedDate: string;

    mainImage?: {

      id: number;

      url: string;

      formats?: {

        thumbnail?: {

          url: string;

        };

        small?: {

          url: string;

        };

      };

    };

}


const ArticleListScreen = () => {

  const [articles, setArticles] = useState<Article[]>([]);

  const [loading, setLoading] = useState(true);

  const [error, setError] = useState<string | null>(null);

  const [refreshing, setRefreshing] = useState(false);

  const [page, setPage] = useState(1);

  const [totalPages, setTotalPages] = useState(1);


  const fetchArticles = async (isRefreshing = false) => {

    try {

      if (isRefreshing) {

        setRefreshing(true);

        setPage(1);

      } else {

        setLoading(true);

      }

      setError(null);

      

      const response = await axios.get('/articles', {

        params: {

          populate: '*',

          sort: 'publishedDate:desc',

          'pagination[page]': isRefreshing ? 1 : page,

          'pagination[pageSize]': 10,

          publicationState: 'live'

        }

      });

  

      console.log('API Response:', JSON.stringify(response.data, null, 2));

  

      if (!response.data?.data || !Array.isArray(response.data.data)) {

        throw new Error('Invalid API response structure');

      }

  

      const transformedArticles = response.data.data

        .filter(apiArticle => apiArticle?.title) // Check for title at root level

        .map((apiArticle) => ({

          id: apiArticle.id,

          title: apiArticle.title || 'Untitled Article',

          slug: apiArticle.slug || '',

          publishedBy: apiArticle.publishedBy || 'Unknown Author',

          publishedDate: apiArticle.publishedDate || new Date().toISOString(),

          mainImage: apiArticle.mainImage

        }));

  

      if (transformedArticles.length === 0) {

        throw new Error('No articles found');

      }

  

      if (isRefreshing || page === 1) {

        setArticles(transformedArticles);

      } else {

        setArticles(prev => [...prev, ...transformedArticles]);

      }

  

      setTotalPages(response.data.meta?.pagination?.pageCount || 1);

    } catch (err) {

      console.error('API Error:', err);

      setError(err.response?.data?.error?.message || 

               err.message || 

               'Failed to load articles. Please try again.');

      if (isRefreshing || page === 1) {

        setArticles([]);

      }

    } finally {

      setLoading(false);

      setRefreshing(false);

    }

  };


  useEffect(() => {

    fetchArticles();

  }, [page]);


  const onRefresh = () => {

    fetchArticles(true);

  };


  const loadMoreArticles = () => {

    if (page < totalPages && !loading) {

      setPage(prev => prev + 1);

    }

  };


  const renderArticleItem = ({ item }: { item: Article }) => {

    if (!item) return null;

  

    let imageUrl;

    if (item.mainImage) {

      imageUrl = item.mainImage.formats?.small?.url || 

                item.mainImage.url;

    }

  

    return (

      <Link href={`/articles/${item.slug || ''}`} asChild>

        <TouchableOpacity style={styles.articleCard}>

          {imageUrl ? (

            <Image

              source={{ 

                uri: imageUrl.startsWith('http') 

                  ? imageUrl 

                  : `${axios.defaults.baseURL?.replace('/api', '')}${imageUrl}`

              }}

              style={styles.articleImage}

              resizeMode="cover"

              defaultSource={require('../assets/placeholder-image.png')}

            />

          ) : (

            <Image

              source={require('../assets/placeholder-image.png')}

              style={styles.articleImage}

              resizeMode="cover"

            />

          )}

          <View style={styles.articleContent}>

            <Text style={styles.articleTitle}>{item.title || 'Untitled Article'}</Text>

            <Text style={styles.articleAuthor}>{item.publishedBy || 'Unknown Author'}</Text>

            <Text style={styles.articleDate}>

              {item.publishedDate ? 

                new Date(item.publishedDate).toLocaleDateString('en-US', {

                  year: 'numeric',

                  month: 'long',

                  day: 'numeric'

                }) : 'Date not available'}

            </Text>

          </View>

        </TouchableOpacity>

      </Link>

    );

  };


  const renderFooter = () => {

    if (!loading || page === 1) return null;

    return (

      <View style={styles.footer}>

        <ActivityIndicator size="small" color="#2E7D32" />

      </View>

    );

  };


  if (loading && page === 1 && !refreshing) {

    return (

      <View style={styles.centerContainer}>

        <ActivityIndicator size="large" color="#2E7D32" />

        <Text style={styles.loadingText}>Loading articles...</Text>

      </View>

    );

  }


  if (error && articles.length === 0) {

    return (

      <View style={styles.centerContainer}>

        <Ionicons name="warning-outline" size={40} color="#dc3545" />

        <Text style={styles.errorText}>{error}</Text>

        <TouchableOpacity 

          style={styles.retryButton}

          onPress={() => fetchArticles(true)}

        >

          <Text style={styles.retryButtonText}>Try Again</Text>

        </TouchableOpacity>

      </View>

    );

  }


  if (articles.length === 0 && !loading) {

    return (

      <View style={styles.centerContainer}>

        <Ionicons name="newspaper-outline" size={50} color="#6c757d" />

        <Text style={styles.emptyText}>No articles found</Text>

        <TouchableOpacity 

          style={styles.refreshButton}

          onPress={onRefresh}

        >

          <Text style={styles.refreshButtonText}>Refresh</Text>

        </TouchableOpacity>

      </View>

    );

  }


  return (

    <FlatList

      data={articles}

      keyExtractor={(item) => item.id.toString()}

      renderItem={renderArticleItem}

      refreshControl={

        <RefreshControl

          refreshing={refreshing}

          onRefresh={onRefresh}

          colors={['#2E7D32']}

          tintColor="#2E7D32"

        />

      }

      contentContainerStyle={styles.listContainer}

      ItemSeparatorComponent={() => <View style={styles.separator} />}

      ListFooterComponent={renderFooter}

      onEndReached={loadMoreArticles}

      onEndReachedThreshold={0.5}

    />

  );

};


const styles = StyleSheet.create({

  centerContainer: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

    padding: 20,

  },

  loadingText: {

    marginTop: 16,

    color: '#2E7D32',

    fontSize: 16,

  },

  errorText: {

    color: '#dc3545',

    fontSize: 16,

    marginVertical: 16,

    textAlign: 'center',

  },

  retryButton: {

    backgroundColor: '#2E7D32',

    paddingHorizontal: 24,

    paddingVertical: 12,

    borderRadius: 6,

  },

  retryButtonText: {

    color: 'white',

    fontWeight: '600',

  },

  emptyText: {

    fontSize: 18,

    color: '#6c757d',

    marginTop: 16,

  },

  refreshButton: {

    borderWidth: 1,

    borderColor: '#2E7D32',

    paddingHorizontal: 24,

    paddingVertical: 8,

    borderRadius: 6,

    marginTop: 16,

  },

  refreshButtonText: {

    color: '#2E7D32',

    fontWeight: '600',

  },

  listContainer: {

    padding: 16,

    paddingBottom: Platform.OS === 'ios' ? 80 : 60,

  },

  articleCard: {

    backgroundColor: 'white',

    borderRadius: 12,

    overflow: 'hidden',

    ...Platform.select({

      ios: {

        shadowColor: '#000',

        shadowOffset: { width: 0, height: 2 },

        shadowOpacity: 0.1,

        shadowRadius: 6,

      },

      android: {

        elevation: 2,

      },

      web: {

        boxShadow: '0px 2px 6px rgba(0, 0, 0, 0.1)',

      }

    }),

  },

  articleImage: {

    width: '100%',

    height: 200,

    backgroundColor: '#f5f5f5',

  },

  articleContent: {

    padding: 16,

  },

  articleTitle: {

    fontSize: 18,

    fontWeight: 'bold',

    marginBottom: 8,

    color: '#212529',

  },

  articleAuthor: {

    fontSize: 14,

    color: '#495057',

    marginBottom: 4,

  },

  articleDate: {

    fontSize: 12,

    color: '#6c757d',

  },

  separator: {

    height: 16,

  },

  footer: {

    padding: 20,

    justifyContent: 'center',

    alignItems: 'center',

  },

});


export default ArticleListScreen;


|---CompalyListScreen.tsx

// app/CompanyListScreen.tsx

import React from 'react';

import { View, Text, StyleSheet } from 'react-native';


const CompanyListScreen = () => {

  return (

    <View style={styles.container}>

      <Text style={styles.title}>Company List</Text>

      {/* Add your company list content here */}

    </View>

  );

};


const styles = StyleSheet.create({

  container: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

    padding: 20,

    backgroundColor: 'white',

  },

  title: {

    fontSize: 24,

    fontWeight: 'bold',

  },

});


export default CompanyListScreen;


|---CropListScreen.tsx

// app/CropListScreen.tsx

import React from 'react';

import { View, Text, StyleSheet } from 'react-native';


const CropListScreen = () => {

  return (

    <View style={styles.container}>

      <Text style={styles.title}>Crop List</Text>

      {/* Add your crop list content here */}

    </View>

  );

};


const styles = StyleSheet.create({

  container: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

    padding: 20,

    backgroundColor: 'white',

  },

  title: {

    fontSize: 24,

    fontWeight: 'bold',

  },

});


export default CropListScreen;


|---HomeScreen.tsx

// app/HomeScreen.tsx

import React, { useEffect, useState } from 'react';

import { View, Text, StyleSheet, TouchableOpacity, ImageBackground } from 'react-native';

import { useRouter } from 'expo-router';

import Storage from '../utils/storage';

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import Ionicons from 'react-native-vector-icons/Ionicons';


// Import screens for each tab

import CompanyListScreen from './CompanyListScreen';

import CropListScreen from './CropListScreen';

import ArticleListScreen from './ArticleListScreen';


const Tab = createBottomTabNavigator();


export default function HomeScreen() {

  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const [userName, setUserName] = useState('');

  const router = useRouter();


  useEffect(() => {

    const checkAuthentication = async () => {

      const jwt = await Storage.getItem('jwt');

      const user = await Storage.getItem('user');

      if (jwt) {

        setIsAuthenticated(true);

        if (user) {

          setUserName(JSON.parse(user).name || 'User');

        }

      } else {

        router.replace('/LoginScreen');

      }

    };


    checkAuthentication();

  }, [router]);


  const handleLogout = async () => {

    await Storage.removeItem('jwt');

    await Storage.removeItem('user');

    router.replace('/LoginScreen');

  };


  return (

    <ImageBackground 

      source={require('../assets/images/partial-react-logo.png')} 

      style={styles.background}

      blurRadius={2}

    >

      <View style={styles.overlay}>

        {isAuthenticated ? (

          <>

            <View style={styles.header}>

              <View style={styles.headerContent}>

                <Text style={styles.greeting}>Welcome back,</Text>

                <Text style={styles.userName}>{userName}</Text>

              </View>

              <TouchableOpacity onPress={handleLogout} style={styles.logoutButton}>

                <Ionicons name="log-out-outline" size={24} color="white" />

              </TouchableOpacity>

            </View>


            {/* Bottom Tab Navigator */}

            <Tab.Navigator

              screenOptions={({ route }) => ({

                tabBarIcon: ({ focused, color, size }) => {

                  let iconName;


                  if (route.name === 'Vertical Farming') {

                    iconName = focused ? 'leaf' : 'leaf-outline';

                  } else if (route.name === 'Crop') {

                    iconName = focused ? 'shield-checkmark' : 'shield-checkmark-outline';

                  } else if (route.name === 'Companies') {

                    iconName = focused ? 'business' : 'business-outline';

                  }


                  return <Ionicons name={iconName} size={size} color={color} />;

                },

                tabBarActiveTintColor: '#2E7D32',

                tabBarInactiveTintColor: '#757575',

                tabBarStyle: {

                  backgroundColor: 'white',

                  borderTopWidth: 0,

                  elevation: 10,

                  shadowOpacity: 0.1,

                  shadowRadius: 10,

                  shadowOffset: { width: 0, height: -5 },

                  height: 70,

                  paddingBottom: 10,

                },

                tabBarLabelStyle: {

                  fontSize: 12,

                  marginBottom: 5,

                },

                headerShown: false,

              })}

            >

              <Tab.Screen 

                name="Vertical Farming" 

                component={ArticleListScreen} 

                options={{ 

                  tabBarLabel: 'Articles',

                }}

              />

              <Tab.Screen 

                name="Crop" 

                component={CropListScreen} 

                options={{ 

                  tabBarLabel: 'Crops',

                }}

              />

              <Tab.Screen 

                name="Companies" 

                component={CompanyListScreen} 

                options={{ 

                  tabBarLabel: 'Companies',

                }}

              />

            </Tab.Navigator>

          </>

        ) : (

          <View style={styles.loadingContainer}>

            <Ionicons name="leaf" size={50} color="#2E7D32" />

            <Text style={styles.loadingText}>Loading your farm data...</Text>

          </View>

        )}

      </View>

    </ImageBackground>

  );

}


const styles = StyleSheet.create({

  background: {

    flex: 1,

    resizeMode: 'cover',

  },

  overlay: {

    flex: 1,

    backgroundColor: 'rgba(255, 255, 255, 0.9)',

  },

  header: {

    flexDirection: 'row',

    justifyContent: 'space-between',

    alignItems: 'center',

    paddingHorizontal: 20,

    paddingTop: 50,

    paddingBottom: 20,

    borderBottomLeftRadius: 20,

    borderBottomRightRadius: 20,

    backgroundColor: '#2E7D32',

    shadowColor: '#000',

    shadowOffset: { width: 0, height: 4 },

    shadowOpacity: 0.1,

    shadowRadius: 6,

    elevation: 5,

  },

  headerContent: {

    flex: 1,

  },

  greeting: {

    fontSize: 16,

    color: 'white',

    opacity: 0.9,

  },

  userName: {

    fontSize: 24,

    fontWeight: 'bold',

    color: 'white',

    marginTop: 5,

  },

  logoutButton: {

    padding: 10,

    borderRadius: 20,

    backgroundColor: 'rgba(255, 255, 255, 0.2)',

  },

  loadingContainer: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

  },

  loadingText: {

    marginTop: 20,

    fontSize: 18,

    color: '#2E7D32',

    fontWeight: '500',

  },

});

|---index.tsx

import { useEffect } from 'react';

import { View, Text, StyleSheet } from 'react-native';

import { useRouter } from 'expo-router';


export default function SplashScreen() {

  const router = useRouter();


  useEffect(() => {

    // Wait for 10 seconds before navigating to LoginScreen

    const timer = setTimeout(() => {

      router.replace('/LoginScreen'); // Navigate to LoginScreen after splash

    }, 3000); // 10 seconds delay


    // Clean up timeout if the component unmounts before 10 seconds

    return () => clearTimeout(timer);

  }, [router]);


  return (

    <View style={styles.container}>

      <Text style={styles.text}>VertiGrow</Text>

    </View>

  );

}


const styles = StyleSheet.create({

  container: {

    flex: 1,

    justifyContent: 'center',

    alignItems: 'center',

    backgroundColor: 'white', // Set splash background color

  },

  text: {

    fontSize: 32,

    fontWeight: 'bold',

  },

});


|---LoginScreen.tsx

import React, { useState, useEffect } from 'react';

import { View, TextInput, Text, StyleSheet, TouchableOpacity, Animated, Platform } from 'react-native';

import { useRouter } from 'expo-router';

import axios from '../utils/axios';

import Storage from '../utils/storage';

import { LinearGradient } from 'expo-linear-gradient';

import { MaterialIcons, MaterialCommunityIcons } from '@expo/vector-icons';


const LoginScreen = () => {

  const [username, setUsername] = useState('');

  const [password, setPassword] = useState('');

  const [isLoading, setIsLoading] = useState(false);

  const router = useRouter();

  

  // Animation refs

  const scaleValue = React.useRef(new Animated.Value(1)).current;

  const fadeValue = React.useRef(new Animated.Value(0)).current;

  const slideValue = React.useRef(new Animated.Value(20)).current;


  useEffect(() => {

    const animation = Animated.parallel([

      Animated.timing(fadeValue, {

        toValue: 1,

        duration: 800,

        useNativeDriver: true,

      }),

      Animated.spring(slideValue, {

        toValue: 0,

        damping: 10,

        useNativeDriver: true,

      })

    ]);


    animation.start();


    return () => {

      animation.stop();

    };

  }, []);


  const handlePressIn = () => {

    Animated.spring(scaleValue, {

      toValue: 0.95,

      useNativeDriver: true,

    }).start();

  };


  const handlePressOut = () => {

    Animated.spring(scaleValue, {

      toValue: 1,

      friction: 3,

      tension: 40,

      useNativeDriver: true,

    }).start();

  };


  const handleLogin = async () => {

    if (isLoading) return;

    

    if (!username || !password) {

      showErrorAnimation();

      alert('Please fill in all fields');

      return;

    }


    setIsLoading(true);


    try {

      const response = await axios.post('/auth/local', {

        identifier: username,

        password,

      });


      if (response.data?.jwt) {

        await Storage.setItem('jwt', response.data.jwt);

        router.replace('/HomeScreen');

      }

    } catch (error) {

      console.error('Login error:', error);

      showErrorAnimation();

      alert('Invalid credentials, please try again.');

    } finally {

      setIsLoading(false);

    }

  };


  const showErrorAnimation = () => {

    Animated.sequence([

      Animated.timing(slideValue, {

        toValue: 10,

        duration: 50,

        useNativeDriver: true,

      }),

      Animated.timing(slideValue, {

        toValue: -10,

        duration: 50,

        useNativeDriver: true,

      }),

      Animated.spring(slideValue, {

        toValue: 0,

        damping: 10,

        useNativeDriver: true,

      }),

    ]).start();

  };


  return (

    <LinearGradient

      colors={['#e8f5e9', '#c8e6c9', '#a5d6a7']}

      style={styles.container}

    >

      <Animated.View 

        style={[

          styles.animatedContainer,

          { 

            opacity: fadeValue,

            transform: [{ translateY: slideValue }],

          }

        ]}

      >

        <MaterialCommunityIcons 

          name="sprout" 

          size={80} 

          color="#2e7d32" 

          style={styles.logoIcon}

        />

        

        <Text style={styles.title}>Welcome Back</Text>

        <Text style={styles.subtitle}>Continue your VertiGrow journey</Text>

        

        <View style={styles.inputContainer}>

          <MaterialIcons name="email" size={24} color="#2e7d32" />

          <TextInput

            style={styles.input}

            placeholder="Email"

            placeholderTextColor="#81c784"

            value={username}

            onChangeText={setUsername}

            keyboardType="email-address"

            autoCapitalize="none"

            autoComplete="email"

            textContentType="emailAddress"

            editable={!isLoading}

          />

        </View>

        

        <View style={styles.inputContainer}>

          <MaterialIcons name="lock" size={24} color="#2e7d32" />

          <TextInput

            style={styles.input}

            placeholder="Password"

            placeholderTextColor="#81c784"

            value={password}

            onChangeText={setPassword}

            secureTextEntry

            autoComplete="password"

            textContentType="password"

            editable={!isLoading}

          />

        </View>

        

        <Animated.View style={{ transform: [{ scale: scaleValue }] }}>

          <TouchableOpacity

            style={[styles.loginButton, isLoading && styles.disabledButton]}

            onPress={handleLogin}

            onPressIn={handlePressIn}

            onPressOut={handlePressOut}

            activeOpacity={0.7}

            disabled={isLoading}

          >

            <Text style={styles.buttonText}>

              {isLoading ? 'LOGGING IN...' : 'LOGIN'}

            </Text>

          </TouchableOpacity>

        </Animated.View>

        

        <TouchableOpacity 

          onPress={() => router.push('/SignupScreen')}

          style={styles.signupLinkContainer}

          disabled={isLoading}

        >

          <Text style={styles.signupText}>

            Don't have an account? <Text style={styles.signupLink}>Sign up</Text>

          </Text>

        </TouchableOpacity>

        

        <View style={styles.plantsContainer}>

          <MaterialCommunityIcons 

            name="leaf" 

            size={40} 

            color="#2e7d32" 

            style={[styles.plantIcon, styles.plantLeft]} 

          />

          <MaterialCommunityIcons 

            name="sprout-outline" 

            size={60} 

            color="#388e3c" 

            style={styles.plantCenter} 

          />

          <MaterialCommunityIcons 

            name="leaf" 

            size={40} 

            color="#2e7d32" 

            style={[styles.plantIcon, styles.plantRight]} 

          />

        </View>

      </Animated.View>

    </LinearGradient>

  );

};


const styles = StyleSheet.create({

  container: {

    flex: 1,

    justifyContent: 'center',

    padding: 20,

  },

  animatedContainer: {

    width: '100%',

    alignItems: 'center',

  },

  logoIcon: {

    marginBottom: 20,

  },

  title: {

    fontSize: 28,

    fontWeight: 'bold',

    color: '#2e7d32',

    marginBottom: 5,

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  subtitle: {

    fontSize: 16,

    color: '#81c784',

    marginBottom: 30,

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  inputContainer: {

    flexDirection: 'row',

    alignItems: 'center',

    width: '100%',

    marginBottom: 15,

    backgroundColor: 'rgba(255, 255, 255, 0.8)',

    borderRadius: 25,

    paddingHorizontal: 20,

    paddingVertical: 5,

  },

  input: {

    flex: 1,

    height: 50,

    color: '#2e7d32',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

    marginLeft: 10,

  },

  loginButton: {

    backgroundColor: '#2e7d32',

    paddingVertical: 15,

    paddingHorizontal: 40,

    borderRadius: 25,

    marginTop: 20,

    shadowColor: '#000',

    shadowOffset: { width: 0, height: 2 },

    shadowOpacity: 0.3,

    shadowRadius: 3,

    elevation: 5,

  },

  disabledButton: {

    opacity: 0.7,

  },

  buttonText: {

    color: 'white',

    fontWeight: 'bold',

    fontSize: 16,

    textAlign: 'center',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  signupLinkContainer: {

    marginTop: 20,

  },

  signupText: {

    color: '#81c784',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  signupLink: {

    color: '#2e7d32',

    fontWeight: 'bold',

  },

  plantsContainer: {

    flexDirection: 'row',

    marginTop: 40,

    alignItems: 'flex-end',

    height: 80,

  },

  plantIcon: {

    opacity: 0.8,

  },

  plantLeft: {

    transform: [{ rotate: '-30deg' }],

    marginRight: -15,

  },

  plantCenter: {

    zIndex: 1,

  },

  plantRight: {

    transform: [{ rotate: '30deg' }],

    marginLeft: -15,

  },

});


export default LoginScreen;


|---SignupScreen.tsx

import React, { useState, useEffect } from 'react';

import { View, TextInput, Text, StyleSheet, TouchableOpacity, Animated, Platform } from 'react-native';

import { useRouter } from 'expo-router';

import axios from '../utils/axios';

import Storage from '../utils/storage';

import { LinearGradient } from 'expo-linear-gradient';

import { MaterialIcons, MaterialCommunityIcons } from '@expo/vector-icons';


const SignupScreen = () => {

  const [username, setUsername] = useState('');

  const [password, setPassword] = useState('');

  const [confirmPassword, setConfirmPassword] = useState('');

  const [isLoading, setIsLoading] = useState(false);

  const router = useRouter();

  

  // Animation refs

  const scaleValue = React.useRef(new Animated.Value(1)).current;

  const fadeValue = React.useRef(new Animated.Value(0)).current;

  const slideValue = React.useRef(new Animated.Value(20)).current;


  useEffect(() => {

    const animation = Animated.parallel([

      Animated.timing(fadeValue, {

        toValue: 1,

        duration: 800,

        useNativeDriver: true,

      }),

      Animated.spring(slideValue, {

        toValue: 0,

        damping: 10,

        useNativeDriver: true,

      })

    ]);


    animation.start();


    return () => {

      animation.stop();

    };

  }, []);


  const handlePressIn = () => {

    Animated.spring(scaleValue, {

      toValue: 0.95,

      useNativeDriver: true,

    }).start();

  };


  const handlePressOut = () => {

    Animated.spring(scaleValue, {

      toValue: 1,

      friction: 3,

      tension: 40,

      useNativeDriver: true,

    }).start();

  };


  const handleSignup = async () => {

    if (isLoading) return;

    

    if (!username || !password || !confirmPassword) {

      showErrorAnimation();

      alert('Please fill in all fields');

      return;

    }


    if (password !== confirmPassword) {

      showErrorAnimation();

      alert('Passwords do not match');

      return;

    }


    setIsLoading(true);


    try {

      const response = await axios.post('/auth/local/register', {

        username,

        password,

        email: username,

      });


      if (response.data?.jwt) {

        await Storage.setItem('jwt', response.data.jwt);

        router.replace('/HomeScreen');

      }

    } catch (error) {

      console.error('Signup error:', error);

      showErrorAnimation();

      alert('Registration failed. Please try again.');

    } finally {

      setIsLoading(false);

    }

  };


  const showErrorAnimation = () => {

    Animated.sequence([

      Animated.timing(slideValue, {

        toValue: 10,

        duration: 50,

        useNativeDriver: true,

      }),

      Animated.timing(slideValue, {

        toValue: -10,

        duration: 50,

        useNativeDriver: true,

      }),

      Animated.spring(slideValue, {

        toValue: 0,

        damping: 10,

        useNativeDriver: true,

      }),

    ]).start();

  };


  return (

    <LinearGradient

      colors={['#e8f5e9', '#c8e6c9', '#a5d6a7']}

      style={styles.container}

    >

      <Animated.View 

        style={[

          styles.animatedContainer,

          { 

            opacity: fadeValue,

            transform: [{ translateY: slideValue }],

          }

        ]}

      >

        <MaterialCommunityIcons 

          name="sprout" 

          size={80} 

          color="#2e7d32" 

          style={styles.logoIcon}

        />

        

        <Text style={styles.title}>Grow With Us</Text>

        <Text style={styles.subtitle}>Create your VertiGrow account</Text>

        

        <View style={styles.inputContainer}>

          <MaterialIcons name="email" size={24} color="#2e7d32" />

          <TextInput

            style={styles.input}

            placeholder="Email"

            placeholderTextColor="#81c784"

            value={username}

            onChangeText={setUsername}

            keyboardType="email-address"

            autoCapitalize="none"

            autoComplete="email"

            textContentType="emailAddress"

            editable={!isLoading}

          />

        </View>

        

        <View style={styles.inputContainer}>

          <MaterialIcons name="lock" size={24} color="#2e7d32" />

          <TextInput

            style={styles.input}

            placeholder="Password"

            placeholderTextColor="#81c784"

            value={password}

            onChangeText={setPassword}

            secureTextEntry

            autoComplete="password"

            textContentType="password"

            editable={!isLoading}

          />

        </View>

        

        <View style={styles.inputContainer}>

          <MaterialIcons name="lock-outline" size={24} color="#2e7d32" />

          <TextInput

            style={styles.input}

            placeholder="Confirm Password"

            placeholderTextColor="#81c784"

            value={confirmPassword}

            onChangeText={setConfirmPassword}

            secureTextEntry

            autoComplete="password"

            textContentType="password"

            editable={!isLoading}

          />

        </View>

        

        <Animated.View style={{ transform: [{ scale: scaleValue }] }}>

          <TouchableOpacity

            style={[styles.signupButton, isLoading && styles.disabledButton]}

            onPress={handleSignup}

            onPressIn={handlePressIn}

            onPressOut={handlePressOut}

            activeOpacity={0.7}

            disabled={isLoading}

          >

            <Text style={styles.buttonText}>

              {isLoading ? 'CREATING ACCOUNT...' : 'SIGN UP'}

            </Text>

          </TouchableOpacity>

        </Animated.View>

        

        <TouchableOpacity 

          onPress={() => router.push('/LoginScreen')}

          style={styles.loginLinkContainer}

          disabled={isLoading}

        >

          <Text style={styles.loginText}>

            Already have an account? <Text style={styles.loginLink}>Login</Text>

          </Text>

        </TouchableOpacity>

        

        <View style={styles.plantsContainer}>

          <MaterialCommunityIcons 

            name="leaf" 

            size={40} 

            color="#2e7d32" 

            style={[styles.plantIcon, styles.plantLeft]} 

          />

          <MaterialCommunityIcons 

            name="sprout-outline" 

            size={60} 

            color="#388e3c" 

            style={styles.plantCenter} 

          />

          <MaterialCommunityIcons 

            name="leaf" 

            size={40} 

            color="#2e7d32" 

            style={[styles.plantIcon, styles.plantRight]} 

          />

        </View>

      </Animated.View>

    </LinearGradient>

  );

};


const styles = StyleSheet.create({

  container: {

    flex: 1,

    justifyContent: 'center',

    padding: 20,

  },

  animatedContainer: {

    width: '100%',

    alignItems: 'center',

  },

  logoIcon: {

    marginBottom: 20,

  },

  title: {

    fontSize: 28,

    fontWeight: 'bold',

    color: '#2e7d32',

    marginBottom: 5,

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  subtitle: {

    fontSize: 16,

    color: '#81c784',

    marginBottom: 30,

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  inputContainer: {

    flexDirection: 'row',

    alignItems: 'center',

    width: '100%',

    marginBottom: 15,

    backgroundColor: 'rgba(255, 255, 255, 0.8)',

    borderRadius: 25,

    paddingHorizontal: 20,

    paddingVertical: 5,

  },

  input: {

    flex: 1,

    height: 50,

    color: '#2e7d32',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

    marginLeft: 10,

  },

  signupButton: {

    backgroundColor: '#2e7d32',

    paddingVertical: 15,

    paddingHorizontal: 40,

    borderRadius: 25,

    marginTop: 20,

    shadowColor: '#000',

    shadowOffset: { width: 0, height: 2 },

    shadowOpacity: 0.3,

    shadowRadius: 3,

    elevation: 5,

  },

  disabledButton: {

    opacity: 0.7,

  },

  buttonText: {

    color: 'white',

    fontWeight: 'bold',

    fontSize: 16,

    textAlign: 'center',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  loginLinkContainer: {

    marginTop: 20,

  },

  loginText: {

    color: '#81c784',

    fontFamily: Platform.OS === 'ios' ? 'Avenir' : 'Roboto',

  },

  loginLink: {

    color: '#2e7d32',

    fontWeight: 'bold',

  },

  plantsContainer: {

    flexDirection: 'row',

    marginTop: 40,

    alignItems: 'flex-end',

    height: 80,

  },

  plantIcon: {

    opacity: 0.8,

  },

  plantLeft: {

    transform: [{ rotate: '-30deg' }],

    marginRight: -15,

  },

  plantCenter: {

    zIndex: 1,

  },

  plantRight: {

    transform: [{ rotate: '30deg' }],

    marginLeft: -15,

  },

});


export default SignupScreen;

|--utils

|---axios.ts

// utils/axios.ts

import axios from 'axios';


const instance = axios.create({

  baseURL: 'http://192.168.29.56:1337/api', // Use your local IP and append /api

  headers: {

    'Content-Type': 'application/json',

  },

});


export default instance;


|---storage.ts

// utils/storage.ts

import { Platform } from 'react-native';

import AsyncStorage from '@react-native-async-storage/async-storage';


const Storage = {

  // Save item based on platform (AsyncStorage for mobile, localStorage for web)

  async setItem(key: string, value: string) {

    if (Platform.OS === 'web') {

      localStorage.setItem(key, value);

      // Trigger storage event to notify other tabs

      window.dispatchEvent(new Event('storage'));

    } else {

      await AsyncStorage.setItem(key, value);

    }

  },


  // Get item based on platform (AsyncStorage for mobile, localStorage for web)

  async getItem(key: string) {

    if (Platform.OS === 'web') {

      return localStorage.getItem(key);

    } else {

      return await AsyncStorage.getItem(key);

    }

  },


  // Remove item based on platform (AsyncStorage for mobile, localStorage for web)

  async removeItem(key: string) {

    if (Platform.OS === 'web') {

      localStorage.removeItem(key);

      // Trigger storage event to notify other tabs

      window.dispatchEvent(new Event('storage'));

    } else {

      await AsyncStorage.removeItem(key);

    }

  }

};


export default Storage;


Post a Comment

0 Comments