diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 12a72c4..2b31c1c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") @@ -9,6 +12,7 @@ android { namespace = "edu.BitaZoft.harvest_manager" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion + ndkVersion = "29.0.13113456" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 @@ -28,6 +32,8 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + + minSdk = 23 } buildTypes { diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..c536445 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "599479154364", + "project_id": "teatrack-72861", + "storage_bucket": "teatrack-72861.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:599479154364:android:70266cc67307eaa3b102b9", + "android_client_info": { + "package_name": "edu.BitaZoft.harvest_manager" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCVBzfluJLARQcYuk7pVremlZ4MxCl3JcQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/assets/images/Loading round.png b/assets/images/Loading round.png new file mode 100644 index 0000000..a3dcbc0 Binary files /dev/null and b/assets/images/Loading round.png differ diff --git a/assets/images/bottom.png b/assets/images/bottom.png new file mode 100644 index 0000000..476fcf8 Binary files /dev/null and b/assets/images/bottom.png differ diff --git a/assets/images/teacup.png b/assets/images/teacup.png new file mode 100644 index 0000000..45a0526 Binary files /dev/null and b/assets/images/teacup.png differ diff --git a/assets/images/top_left.png b/assets/images/top_left.png new file mode 100644 index 0000000..853950d Binary files /dev/null and b/assets/images/top_left.png differ diff --git a/assets/images/top_right.png b/assets/images/top_right.png new file mode 100644 index 0000000..88787c9 Binary files /dev/null and b/assets/images/top_right.png differ diff --git a/lib/core/theme/theme.dart b/lib/core/theme/theme.dart new file mode 100644 index 0000000..a1ee860 --- /dev/null +++ b/lib/core/theme/theme.dart @@ -0,0 +1,477 @@ +import 'package:flutter/material.dart'; + +class TeaTrackerTheme { + // Primary Colors from your Figma design + static const Color primaryGreen = Color(0xFF4CAF50); // Main green from buttons + static const Color darkGreen = Color(0xFF2E7D32); // Dark green for text/borders + static const Color lightGreen = Color(0xFF81C784); // Light green for accents + static const Color paleGreen = Color(0xFF66BB6A); // Medium green + static const Color teaGreen = Color(0xFF8BC34A); // Tea leaf green + + // Background Colors + static const Color creamBackground = Color(0xFFF1F8E9); // Main cream/light green background + static const Color whiteBackground = Color(0xFFFFFFFF); + static const Color cardBackground = Color(0xFFE8F5E8); // Light green card background + static const Color overlayBackground = Color(0xFFF5F5F5); // Light overlay + + // Text Colors + static const Color primaryText = Color(0xFF1B5E20); // Dark green text + static const Color secondaryText = Color(0xFF424242); // Dark gray text + static const Color lightText = Color(0xFF757575); // Light gray text + static const Color whiteText = Color(0xFFFFFFFF); + static const Color placeholderText = Color(0xFF9E9E9E); // Placeholder text + + // Accent Colors + static const Color errorRed = Color(0xFFE53935); // Red for errors/required fields + static const Color warningOrange = Color(0xFFFF9800); + static const Color successGreen = Color(0xFF4CAF50); + static const Color yellowAccent = Color(0xFFFFEB3B); // Yellow from color palette + + // Dashboard Colors + static const Color dashboardCard1 = Color(0xFFE8F5E8); // Light green + static const Color dashboardCard2 = Color(0xFFE1F5FE); // Light blue tint + static const Color dashboardCard3 = Color(0xFFF3E5F5); // Light purple tint + + // Bottom Navigation Colors + static const Color bottomNavBackground = Color(0xFF2E7D32); + static const Color bottomNavSelected = Color(0xFFFFFFFF); + static const Color bottomNavUnselected = Color.fromARGB(179, 0, 0, 0); + + // Gradient Colors + static const LinearGradient backgroundGradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFF1F8E9), + Color(0xFFE8F5E8), + ], + ); + + static const LinearGradient buttonGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF66BB6A), + Color(0xFF4CAF50), + ], + ); + + static const LinearGradient cardGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFFFFF), + Color(0xFFF8F8F8), + ], + ); + + // Tea Cup Icon Colors (for branding) + static const Color teaCupPrimary = Color(0xFF8D6E63); // Brown for tea cup + static const Color teaCupSecondary = Color(0xFF4CAF50); // Green tea color + static const Color teaLeafGreen = Color(0xFF689F38); // Tea leaf green + + // Text Styles + static const TextStyle appTitle = TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: primaryText, + fontFamily: 'Inter', + letterSpacing: -0.5, + ); + + static const TextStyle welcomeTitle = TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: primaryText, + fontFamily: 'Inter', + ); + + static const TextStyle screenTitle = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: primaryText, + fontFamily: 'Inter', + ); + + static const TextStyle sectionTitle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: primaryText, + fontFamily: 'Inter', + ); + + static const TextStyle cardTitle = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: primaryText, + fontFamily: 'Inter', + ); + + static const TextStyle bodyLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: secondaryText, + fontFamily: 'Inter', + ); + + static const TextStyle bodyMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: secondaryText, + fontFamily: 'Inter', + ); + + static const TextStyle bodySmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: lightText, + fontFamily: 'Inter', + ); + + static const TextStyle buttonText = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: whiteText, + fontFamily: 'Inter', + ); + + static const TextStyle linkText = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: primaryGreen, + decoration: TextDecoration.underline, + fontFamily: 'Inter', + ); + + // Dashboard specific text styles + static const TextStyle dashboardMetricNumber = TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: primaryGreen, + fontFamily: 'Inter', + ); + + static const TextStyle dashboardMetricLabel = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: secondaryText, + fontFamily: 'Inter', + ); + + static const TextStyle dashboardSubtitle = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: lightText, + fontFamily: 'Inter', + ); + + // Form field styles + static const TextStyle fieldLabel = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: primaryText, + fontFamily: 'Inter', + ); + + static const TextStyle fieldError = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: errorRed, + fontFamily: 'Inter', + ); + + // Input Decoration + static InputDecoration inputDecoration({ + required String labelText, + String? hintText, + IconData? prefixIcon, + Widget? suffixIcon, + bool isRequired = false, + }) { + return InputDecoration( + labelText: labelText, + hintText: hintText, + prefixIcon: prefixIcon != null ? Icon(prefixIcon, color: primaryGreen) : null, + suffixIcon: suffixIcon, + filled: true, + fillColor: whiteBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: lightGreen, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: lightGreen, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: primaryGreen, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: errorRed, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: errorRed, width: 2), + ), + labelStyle: fieldLabel, + hintStyle: TextStyle(color: placeholderText), + errorStyle: fieldError, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ); + } + + // Button Styles + static final ElevatedButtonThemeData elevatedButtonTheme = ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryGreen, + foregroundColor: whiteText, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + shadowColor: primaryGreen.withOpacity(0.3), + textStyle: buttonText, + ), + ); + + static final OutlinedButtonThemeData outlinedButtonTheme = OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: primaryGreen, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + side: const BorderSide(color: primaryGreen, width: 1.5), + textStyle: buttonText.copyWith(color: primaryGreen), + ), + ); + + // Card Theme + static final CardTheme cardTheme = CardTheme( + color: whiteBackground, + elevation: 4, + shadowColor: primaryGreen.withOpacity(0.15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ); + + // AppBar Theme + static const AppBarTheme appBarTheme = AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + iconTheme: IconThemeData(color: primaryText), + titleTextStyle: screenTitle, + centerTitle: true, + ); + + // Bottom Navigation Bar Theme + static final BottomNavigationBarThemeData bottomNavTheme = BottomNavigationBarThemeData( + backgroundColor: bottomNavBackground, + selectedItemColor: bottomNavSelected, + unselectedItemColor: bottomNavUnselected, + type: BottomNavigationBarType.fixed, + elevation: 8, + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Inter', + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.normal, + fontFamily: 'Inter', + ), + ); + + // Main Theme Data + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + + // Color Scheme + colorScheme: const ColorScheme.light( + primary: primaryGreen, + primaryContainer: lightGreen, + secondary: teaGreen, + secondaryContainer: cardBackground, + surface: whiteBackground, + background: creamBackground, + error: errorRed, + onPrimary: whiteText, + onSecondary: primaryText, + onSurface: primaryText, + onBackground: primaryText, + onError: whiteText, + ), + + // Scaffold Background + scaffoldBackgroundColor: creamBackground, + + // App Bar Theme + appBarTheme: appBarTheme, + + // Button Themes + elevatedButtonTheme: elevatedButtonTheme, + outlinedButtonTheme: outlinedButtonTheme, + + // Card Theme + cardTheme: cardTheme, + + // Bottom Navigation Theme + bottomNavigationBarTheme: bottomNavTheme, + + // Input Decoration Theme + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: whiteBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: lightGreen, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: lightGreen, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: primaryGreen, width: 2), + ), + labelStyle: fieldLabel, + hintStyle: TextStyle(color: placeholderText), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + + // Text Theme + textTheme: const TextTheme( + displayLarge: appTitle, + displayMedium: welcomeTitle, + displaySmall: screenTitle, + headlineLarge: welcomeTitle, + headlineMedium: screenTitle, + headlineSmall: sectionTitle, + titleLarge: sectionTitle, + titleMedium: cardTitle, + titleSmall: bodyLarge, + bodyLarge: bodyLarge, + bodyMedium: bodyMedium, + bodySmall: bodySmall, + labelLarge: buttonText, + labelMedium: bodyMedium, + labelSmall: bodySmall, + ), + + // Icon Theme + iconTheme: const IconThemeData( + color: primaryGreen, + size: 24, + ), + + // Divider Theme + dividerTheme: DividerThemeData( + color: lightGreen.withOpacity(0.3), + thickness: 1, + space: 16, + ), + + // Checkbox Theme + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return primaryGreen; + } + return null; + }), + checkColor: MaterialStateProperty.all(whiteText), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + + // Font Family + fontFamily: 'Inter', + ); + } + + // Custom Box Decorations + static BoxDecoration get authCardDecoration => BoxDecoration( + gradient: cardGradient, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: primaryGreen.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ); + + static BoxDecoration get dashboardCardDecoration => BoxDecoration( + color: whiteBackground, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: primaryGreen.withOpacity(0.08), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ); + + static BoxDecoration get factoryCardDecoration => BoxDecoration( + color: whiteBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: lightGreen.withOpacity(0.3), width: 1), + ); + + static BoxDecoration get backgroundDecoration => const BoxDecoration( + gradient: backgroundGradient, + ); + + static BoxDecoration get buttonDecoration => const BoxDecoration( + gradient: buttonGradient, + borderRadius: BorderRadius.all(Radius.circular(12)), + ); + + // FAB decoration for the centered add button + static BoxDecoration get fabDecoration => BoxDecoration( + gradient: buttonGradient, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: primaryGreen.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ); + + // Welcome screen specific decorations + static BoxDecoration get welcomeBackgroundDecoration => const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFF1F8E9), + Color(0xFFE8F5E8), + ], + ), + ); + + // Chart colors for analytics + static List get chartColors => [ + primaryGreen, + lightGreen, + teaGreen, + paleGreen, + darkGreen, + ]; + + // Status colors + static const Color activeStatus = Color(0xFF4CAF50); + static const Color inactiveStatus = Color(0xFF9E9E9E); + static const Color pendingStatus = Color(0xFFFF9800); + static const Color completedStatus = Color(0xFF66BB6A); +} \ No newline at end of file diff --git a/lib/feature/Home/screens/home_screen.dart b/lib/feature/Home/screens/home_screen.dart new file mode 100644 index 0000000..df75708 --- /dev/null +++ b/lib/feature/Home/screens/home_screen.dart @@ -0,0 +1,19 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Home")), + body: ElevatedButton( + onPressed: () async { + await FirebaseAuth.instance.signOut(); + }, + child: const Text('Logout'), + ), + ); + } +} diff --git a/lib/feature/drawer/app_drawer.dart b/lib/feature/drawer/app_drawer.dart new file mode 100644 index 0000000..6462957 --- /dev/null +++ b/lib/feature/drawer/app_drawer.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:harvest_manager/core/theme/theme.dart'; + +class AppDrawer extends StatelessWidget { + final Function(int) onItemTapped; + + const AppDrawer({ + super.key, + required this.onItemTapped, + }); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + decoration: BoxDecoration( + color: TeaTrackerTheme.darkGreen, + ), + child: Text( + 'Menu', + style: TextStyle( + color: TeaTrackerTheme.whiteBackground, + fontSize: 24, + ), + ), + ), + ListTile( + leading: const Icon(CupertinoIcons.profile_circled), + title: const Text('Profile'), + onTap: () { + Navigator.pop(context); + onItemTapped(4); + }, + ), + ListTile( + leading: const Icon(CupertinoIcons.info), + title: const Text('About'), + onTap: () { + Navigator.pop(context); + + }, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/feature/home/screens/home_screen.dart b/lib/feature/home/screens/home_screen.dart new file mode 100644 index 0000000..df75708 --- /dev/null +++ b/lib/feature/home/screens/home_screen.dart @@ -0,0 +1,19 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Home")), + body: ElevatedButton( + onPressed: () async { + await FirebaseAuth.instance.signOut(); + }, + child: const Text('Logout'), + ), + ); + } +} diff --git a/lib/feature/navbar/bottom_navbar.dart b/lib/feature/navbar/bottom_navbar.dart new file mode 100644 index 0000000..f233b28 --- /dev/null +++ b/lib/feature/navbar/bottom_navbar.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:harvest_manager/core/theme/theme.dart'; +import 'package:harvest_manager/feature/drawer/app_drawer.dart'; + +class BottomNavbar extends StatefulWidget { + const BottomNavbar({super.key}); + + @override + State createState() => _BottomNavbarState(); +} + +class _BottomNavbarState extends State { + int _selectedIndex = 0; + + static final List _screens = [ + Container(child: const Center(child: Text('Home Screen'))), // index 0 + Container(child: const Center(child: Text('Analytics Screen'))), // index 1 + Container(child: const Center(child: Text('Add'))), // index 2 + Container(child: const Center(child: Text('Notifications Screen'))), // index 3 + Container(child: const Center(child: Text('Menu Screen'))), // index 4 + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: TeaTrackerTheme.creamBackground, + body: _screens[_selectedIndex], + drawer: AppDrawer(onItemTapped: _onItemTapped), // Use the separate drawer + bottomNavigationBar: BottomAppBar( + color: TeaTrackerTheme.bottomNavBackground, + elevation: 8, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + buildNavBarItem(CupertinoIcons.home, 'Home', 0), + buildNavBarItem(CupertinoIcons.chart_bar, 'Analytics', 1), + const SizedBox( + width: 20, + ), + buildNavBarItem(CupertinoIcons.bell, 'Notifications', 3), + buildDrawerItem(), + ], + ), + ), + floatingActionButton: Container( + decoration: TeaTrackerTheme.fabDecoration, + child: FloatingActionButton( + onPressed: () => _onItemTapped(2), + backgroundColor: Colors.transparent, + elevation: 0, + child: const Icon( + CupertinoIcons.add, + size: 28, + color: TeaTrackerTheme.whiteText, + ), + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + ); + } + + Widget buildNavBarItem(IconData icon, String label, int index) { + return InkWell( + onTap: () => _onItemTapped(index), + borderRadius: BorderRadius.circular(8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: _selectedIndex == index + ? TeaTrackerTheme.bottomNavSelected + : TeaTrackerTheme.bottomNavUnselected, + ), + Text( + label, + style: TextStyle( + color: _selectedIndex == index + ? TeaTrackerTheme.bottomNavSelected + : TeaTrackerTheme.bottomNavUnselected, + fontFamily: 'Inder', + ), + ), + ], + ), + ); + } + + Widget buildDrawerItem() { + return Builder( + builder: (BuildContext context) { + return InkWell( + onTap: () { + Scaffold.of(context).openDrawer(); + }, + borderRadius: BorderRadius.circular(8), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.line_horizontal_3, + color: TeaTrackerTheme.bottomNavUnselected, + ), + Text( + 'Menu', + style: TextStyle( + color: TeaTrackerTheme.bottomNavUnselected, + ), + ), + ], + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/feature/sign-up/models/base_user.dart b/lib/feature/sign-up/models/base_user.dart new file mode 100644 index 0000000..1d27bcf --- /dev/null +++ b/lib/feature/sign-up/models/base_user.dart @@ -0,0 +1,7 @@ +abstract class BaseUserModel { + String? uid; + + Map? toMap() { + return null; + } +} \ No newline at end of file diff --git a/lib/feature/sign-up/models/signup_exceptions.dart b/lib/feature/sign-up/models/signup_exceptions.dart new file mode 100644 index 0000000..f3e46a7 --- /dev/null +++ b/lib/feature/sign-up/models/signup_exceptions.dart @@ -0,0 +1,23 @@ +/// Custom exception for better error messages +class SignupException implements Exception { + final String message; + SignupException(this.message); + + factory SignupException.fromCode(String code) { + switch (code) { + case 'email-already-in-use': + return SignupException('Email is already in use.'); + case 'invalid-email': + return SignupException('Email address is invalid.'); + case 'operation-not-allowed': + return SignupException('Signup is disabled. Contact admin.'); + case 'weak-password': + return SignupException('The password is too weak.'); + default: + return SignupException('Signup failed. Please try again.'); + } + } + + @override + String toString() => message; +} diff --git a/lib/feature/sign-up/models/user_email_model.dart b/lib/feature/sign-up/models/user_email_model.dart new file mode 100644 index 0000000..8b39dd2 --- /dev/null +++ b/lib/feature/sign-up/models/user_email_model.dart @@ -0,0 +1,44 @@ +import 'package:harvest_manager/feature/sign-up/models/base_user.dart'; + +class UserEmailModel implements BaseUserModel{ + @override + String? uid; + final String fullName; + final String email; + final String address; + final String signInMethod; + final String role; + + UserEmailModel({ + this.uid, + required this.fullName, + required this.email, + required this.address, + required this.signInMethod, + required this.role, + }); + + // Convert Firestore document data into UserEmailModel + factory UserEmailModel.fromMap(String uid, Map data) { + return UserEmailModel( + uid: uid, + fullName: data['fullName'] ?? '', + email: data['email'] ?? 'null', + address: data['address'] ?? '', + signInMethod: data['signInMethod'] ?? 'email', + role: data['role'] ?? "null", + ); + } + + // Convert UserEmailModel to map to save in Firestore + @override + Map toMap() { + return { + 'fullName': fullName, + 'email': email, + 'address': address, + 'signInMethod': signInMethod, + 'role': role, + }; + } +} diff --git a/lib/feature/sign-up/models/user_phone_model.dart b/lib/feature/sign-up/models/user_phone_model.dart new file mode 100644 index 0000000..9abbd92 --- /dev/null +++ b/lib/feature/sign-up/models/user_phone_model.dart @@ -0,0 +1,44 @@ +import 'package:harvest_manager/feature/sign-up/models/base_user.dart'; + +class UserPhoneModel implements BaseUserModel{ + @override + String? uid; + final String fullName; + final String phoneNumber; + final String address; + final String signInMethod; + final String role; + + UserPhoneModel({ + this.uid, + required this.fullName, + required this.phoneNumber, + required this.address, + required this.signInMethod, + required this.role, + }); + + // Convert Firestore document data into UserPhoneModel + factory UserPhoneModel.fromMap(String uid, Map data) { + return UserPhoneModel( + uid: uid, + fullName: data['fullName'] ?? '', + phoneNumber: data['phoneNumber'] ?? 'null', + address: data['address'] ?? '', + signInMethod: data['signInMethod'] ?? 'email', + role: data['role'] ?? 'null', + ); + } + + // Convert UserPhoneModel to map to save in Firestore + @override + Map toMap() { + return { + 'fullName': fullName, + 'phoneNumber': phoneNumber, + 'address': address, + 'signInMethod': signInMethod, + 'role': role, + }; + } +} diff --git a/lib/feature/sign-up/screens/sign_up_screen.dart b/lib/feature/sign-up/screens/sign_up_screen.dart new file mode 100644 index 0000000..5803cbe --- /dev/null +++ b/lib/feature/sign-up/screens/sign_up_screen.dart @@ -0,0 +1,291 @@ +import 'package:delightful_toast/delight_toast.dart'; +import 'package:delightful_toast/toast/utils/enums.dart'; +import 'package:flutter/material.dart'; +import 'package:harvest_manager/feature/sign-up/models/signup_exceptions.dart'; +import 'package:harvest_manager/feature/sign-up/services/auth_service.dart'; +import 'package:harvest_manager/feature/sign-up/widgets/register_button.dart'; +import 'package:harvest_manager/feature/sign-up/widgets/signup_textfield.dart'; +import 'package:harvest_manager/shared/background_template.dart'; +import 'package:delightful_toast/toast/components/toast_card.dart'; + +class SignUpScreen extends StatefulWidget { + const SignUpScreen({super.key}); + + @override + State createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends State { + String _slectedValue = "supplier"; + bool _isLoading = false; + final _formKey = GlobalKey(); + + // Text controllers + final phoneOrEmailController = TextEditingController(); + final nameController = TextEditingController(); + final passwordController = TextEditingController(); + final addressController = TextEditingController(); + final authService = AuthService(); + + Future registerUser() async { + setState(() { + _isLoading = !_isLoading; + }); + try { + await authService.classifyUserInput( + emailOrPhone: phoneOrEmailController.text.trim(), + password: passwordController.text.trim(), + address: addressController.text.trim(), + name: nameController.text.trim(), + role: _slectedValue, + ); + + if (!mounted) return; + + DelightToastBar( + builder: + (context) => const ToastCard( + leading: Icon( + Icons.check_circle_outlined, + size: 28, + color: Colors.white, + ), + title: Text( + "Successfully Registered!!!", + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + color: Colors.white, + ), + ), + color: Color(0xff43A047), + ), + position: DelightSnackbarPosition.top, + autoDismiss: true, + snackbarDuration: Duration(seconds: 2), + ).show(context); + } on SignupException catch (e) { + String err = e.toString(); + if (!mounted) return; + DelightToastBar( + builder: + (context) => ToastCard( + leading: Icon( + Icons.check_circle_outlined, + size: 28, + color: Colors.white, + ), + title: Center( + child: Text( + err, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + color: Colors.white, + ), + ), + ), + color: Color(0xffE53935), + ), + position: DelightSnackbarPosition.top, + autoDismiss: true, + snackbarDuration: Duration(seconds: 2), + ).show(context); + } finally { + if (mounted) { + setState(() => _isLoading = !_isLoading); + } + } + } + + @override + Widget build(BuildContext context) { + return BackgroundTemplate( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset("assets/images/teacup.png"), + Builder( + builder: (context) { + double screenWidth = MediaQuery.of(context).size.width; + return Padding( + padding: EdgeInsets.symmetric( + horizontal: screenWidth * 0.2, + ), + child: Text( + "SignUp", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color.fromRGBO(33, 33, 33, 1), + ), + ), + ); + }, + ), + ], + ), + + // E-mail/Phone number field + SizedBox(height: 25), + Form( + key: _formKey, + child: Column( + children: [ + //email or phone number field + SignupTextfield( + title: "Email / Phone number", + hintText: "Enter your email or phone number", + obsecureText: false, + controller: phoneOrEmailController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email / Phone required'; + } + if (!RegExp( + r'^[a-zA-Z0-9._%+-]+@gmail\.com$', + ).hasMatch(value) && + !RegExp( + r"^0(70|71|72|74|75|76|77|78)\d{7}$", + ).hasMatch(value) && + !RegExp( + r"^\+94(70|71|72|74|75|76|77|78)\d{7}$", + ).hasMatch(value)) { + return 'Enter valid email / phone number'; + } + return null; + }, + ), + + // Name field + SizedBox(height: 15), + SignupTextfield( + controller: nameController, + hintText: "Enter your name", + obsecureText: false, + title: 'Name', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email / Phone required'; + } + return null; + }, + ), + + // Address field + SizedBox(height: 15), + SignupTextfield( + controller: addressController, + hintText: "Enter your address", + obsecureText: false, + title: 'Address', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email / Phone required'; + } + return null; + }, + ), + + //Password field + const SizedBox(height: 15), + SignupTextfield( + title: "Password", + hintText: "Enter your password", + obsecureText: true, + controller: passwordController, + validator: (value) { + if (value == null || value.length < 6) { + return 'Min 6 characters'; + } + return null; + }, + ), + ], + ), + ), + + // Checkboxes for User Role + SizedBox(height: 15), + Text( + 'Role', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0XFF616161), + ), + ), + SizedBox(height: 5), + RadioListTile( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + contentPadding: EdgeInsets.zero, + title: Text("Supplier"), + value: "supplier", + groupValue: _slectedValue, + onChanged: (value) { + setState(() { + _slectedValue = value!; + }); + }, + ), + RadioListTile( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + contentPadding: EdgeInsets.zero, + title: Text("Transporter"), + value: "transporter", + groupValue: _slectedValue, + onChanged: (value) { + setState(() { + _slectedValue = value!; + }); + }, + ), + + //button for register + RegisterButton( + title: "Register", + onPressedFunc: () { + if (_formKey.currentState!.validate()) { + registerUser(); + } + }, + isLoading: _isLoading, + ), + + // terms & conditions + Center( + child: Text( + "By SigningUp, you are agree to our", + style: TextStyle(fontSize: 12), + ), + ), + Center( + child: Text( + "Terms & Privacy Policy", + style: TextStyle(fontSize: 12), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Already have an account? "), + TextButton( + onPressed: () { + Navigator.push( + context, + // routes to login page + MaterialPageRoute(builder: (context) => Container()), + ); + }, + child: Text("Login"), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/feature/sign-up/services/auth_service.dart b/lib/feature/sign-up/services/auth_service.dart new file mode 100644 index 0000000..e5b8994 --- /dev/null +++ b/lib/feature/sign-up/services/auth_service.dart @@ -0,0 +1,80 @@ +import 'package:harvest_manager/feature/sign-up/models/base_user.dart'; +import 'package:harvest_manager/feature/sign-up/models/signup_exceptions.dart'; +import 'package:harvest_manager/feature/sign-up/services/user_service.dart'; +import '../models/user_email_model.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +class AuthService { + final FirebaseAuth _auth = FirebaseAuth.instance; + final UserService _userService = UserService(); + + bool isEmail(String input) => + RegExp(r"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").hasMatch(input); + + bool isPhone(String input) => RegExp(r"^\+?[0-9]{7,15}$").hasMatch(input); + + // clasify user signup using phone number or email + Future classifyUserInput({ + required String emailOrPhone, + required String password, + required String address, + required String name, + required String role, + }) async { + try { + if (isEmail(emailOrPhone)) { + await registerUserWithMail( + user: UserEmailModel( + fullName: name, + email: emailOrPhone, + address: address, + signInMethod: "email", + role: role + ), + mail: emailOrPhone, + password: password, + ); + } else if (isPhone(emailOrPhone)) { + // Add phone auth logic here + // UserPhoneModel( + // fullName: name, + // phoneNumber: emailOrPhone, + // address: address, + // signInMethod: "phone", + // ); + throw SignupException("Phone sign-up is not implemented yet."); + } else { + throw SignupException("Invalid email or phone format."); + } + } on SignupException catch (e) { + throw SignupException(e.message); + } catch (e) { + throw SignupException("Unknown error during sign-up."); + } + } + + /// Registers user with email and password + Future registerUserWithMail({ + required BaseUserModel user, + required String mail, + required String password, + }) async { + try { + // register user using firebase auth + var userCredential = await _auth.createUserWithEmailAndPassword( + email: mail, + password: password, + ); + + //assign registered user uid for store user info + user.uid = userCredential.user?.uid; + + // store user info in firestore db + await _userService.createUserProfile(user); + } on FirebaseAuthException catch (e) { + throw SignupException.fromCode(e.code); + } catch (e) { + throw SignupException('An unexpected error occurred.'); + } + } +} diff --git a/lib/feature/sign-up/services/user_service.dart b/lib/feature/sign-up/services/user_service.dart new file mode 100644 index 0000000..df460c1 --- /dev/null +++ b/lib/feature/sign-up/services/user_service.dart @@ -0,0 +1,36 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:harvest_manager/feature/sign-up/models/base_user.dart'; +import 'package:harvest_manager/feature/sign-up/models/user_email_model.dart'; +import 'package:harvest_manager/feature/sign-up/models/user_phone_model.dart'; + +class UserService { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + // add user data to firestore db + Future createUserProfile(BaseUserModel profile) async { + try { + await _firestore + .collection('users') + .doc(profile.uid) + .set(profile.toMap()!); + } catch (e) { + throw Exception(e.toString()); + } + } + + // retrieve user data from firestore db + Future getUserProfile(String uid) async { + final doc = await _firestore.collection('users').doc(uid).get(); + if (!doc.exists) return null; + + final data = doc.data()!; + + if (data.containsKey('email')) { + return UserEmailModel.fromMap(uid, data); + } else if (data.containsKey('phone')) { + return UserPhoneModel.fromMap(uid, data); + } + + return null; + } +} diff --git a/lib/feature/sign-up/widgets/register_button.dart b/lib/feature/sign-up/widgets/register_button.dart new file mode 100644 index 0000000..a54c611 --- /dev/null +++ b/lib/feature/sign-up/widgets/register_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class RegisterButton extends StatelessWidget { + final String title; + final bool isLoading; + final Function()? onPressedFunc; + + const RegisterButton({ + super.key, + required this.title, + required this.onPressedFunc, + required this.isLoading, + }); + + @override + Widget build(BuildContext context) { + return Builder( + builder: (context) { + double screenWidth = MediaQuery.of(context).size.width; + return Padding( + padding: EdgeInsets.symmetric(vertical: screenWidth * 0.01), + child: ElevatedButton( + onPressed: onPressedFunc, + style: ElevatedButton.styleFrom( + minimumSize: Size(screenWidth * 0.9, 45), + backgroundColor: Color.fromRGBO(46, 125, 50, 0.7), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: + isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + title, + style: TextStyle( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/feature/sign-up/widgets/signup_textfield.dart b/lib/feature/sign-up/widgets/signup_textfield.dart new file mode 100644 index 0000000..ca8105a --- /dev/null +++ b/lib/feature/sign-up/widgets/signup_textfield.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class SignupTextfield extends StatelessWidget { + final TextEditingController controller; + final String hintText; + final String title; + final bool obsecureText; + final String? Function(String?)? validator; + + const SignupTextfield({ + super.key, + required this.controller, + required this.hintText, + required this.obsecureText, + required this.title, + this.validator, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0XFF616161), + ), + ), + const SizedBox(height: 5), + TextFormField( + controller: controller, + obscureText: obsecureText, + validator: validator, + decoration: InputDecoration( + hintText: hintText, + hintStyle: const TextStyle( + fontSize: 16.0, + height: 1.2, + color: Color(0XFF616161), + ), + filled: true, + fillColor: const Color(0XFFD9D9D9), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + ), + style: const TextStyle( + fontSize: 18.0, + height: 1.0, + color: Color.fromARGB(255, 15, 56, 18), + decoration: TextDecoration.none, + ), + ), + ], + ); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..9793b4e --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,89 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyAw9PSd4MvHdkIyOvdR_7dlN5Nq5I9OuEw', + appId: '1:599479154364:web:aaed4642a8ea8b39b102b9', + messagingSenderId: '599479154364', + projectId: 'teatrack-72861', + authDomain: 'teatrack-72861.firebaseapp.com', + storageBucket: 'teatrack-72861.firebasestorage.app', + measurementId: 'G-WTJ3B8BXXQ', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCVBzfluJLARQcYuk7pVremlZ4MxCl3JcQ', + appId: '1:599479154364:android:70266cc67307eaa3b102b9', + messagingSenderId: '599479154364', + projectId: 'teatrack-72861', + storageBucket: 'teatrack-72861.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAD0btVp_ndxIN1Dg92NFuVn1kpiO2C-Mo', + appId: '1:599479154364:ios:10e40d4c092c461db102b9', + messagingSenderId: '599479154364', + projectId: 'teatrack-72861', + storageBucket: 'teatrack-72861.firebasestorage.app', + iosBundleId: 'edu.BitaZoft.harvestManager', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAD0btVp_ndxIN1Dg92NFuVn1kpiO2C-Mo', + appId: '1:599479154364:ios:10e40d4c092c461db102b9', + messagingSenderId: '599479154364', + projectId: 'teatrack-72861', + storageBucket: 'teatrack-72861.firebasestorage.app', + iosBundleId: 'edu.BitaZoft.harvestManager', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyAw9PSd4MvHdkIyOvdR_7dlN5Nq5I9OuEw', + appId: '1:599479154364:web:b1be4d98e06db95ab102b9', + messagingSenderId: '599479154364', + projectId: 'teatrack-72861', + authDomain: 'teatrack-72861.firebaseapp.com', + storageBucket: 'teatrack-72861.firebasestorage.app', + measurementId: 'G-Q2VG31P9F7', + ); + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7b7f5b6..87401de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,122 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:harvest_manager/wrapper.dart'; +import 'firebase_options.dart'; -void main() { + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { + return MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + debugShowCheckedModeBanner: false, + home: Wrapper(), ); } } diff --git a/lib/shared/background_template.dart b/lib/shared/background_template.dart new file mode 100644 index 0000000..f7f29f3 --- /dev/null +++ b/lib/shared/background_template.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +class BackgroundTemplate extends StatelessWidget { + final Widget child; + + const BackgroundTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // Background color container (full screen) + Container( + color: + Color(0xffE9FDD8), + width: double.infinity, + height: double.infinity, + ), + + // Background design images + Positioned( + top: 0, + left: 0, + child: Opacity( + opacity: 0.3, + child: Image.asset('assets/images/top_left.png', width: 170), + ), + ), + Positioned( + top: 0, + right: 0, + child: Opacity( + opacity: 0.5, + child: Image.asset('assets/images/top_right.png', width: 70), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Opacity( + opacity: 0.5, + child: Image.asset( + 'assets/images/bottom.png', + fit: BoxFit.fitWidth, + width: double.infinity, + height: 300, + ), + ), + ), + + // Foreground content (scrollable) + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: child, + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/wrapper.dart b/lib/wrapper.dart new file mode 100644 index 0000000..ee29a9c --- /dev/null +++ b/lib/wrapper.dart @@ -0,0 +1,26 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:harvest_manager/feature/home/screens/home_screen.dart'; +import 'package:harvest_manager/feature/sign-up/screens/sign_up_screen.dart'; + +class Wrapper extends StatelessWidget { + const Wrapper({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: StreamBuilder( + stream: FirebaseAuth.instance.authStateChanges(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasData) { + return const HomeScreen(); // Authenticated + } else { + return const SignUpScreen(); // Not authenticated + } + }, + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..804ef2f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,12 @@ import FlutterMacOS import Foundation +import cloud_firestore +import firebase_auth +import firebase_core func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index c2c57f7..967e312 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "214e6f07e2a44f45972e0365c7b537eaeaddb4598db0778dd4ac64b4acd3f5b1" + url: "https://pub.dev" + source: hosted + version: "1.3.55" async: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: d25c956be5261c14bc9a69c9662de8addb308376b4b53a64469aade52e7b02f8 + url: "https://pub.dev" + source: hosted + version: "5.6.8" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: ee2b8f8c602ede36073afd3741e99cfea9dd982b4a44833daf665134d151c32a + url: "https://pub.dev" + source: hosted + version: "6.6.8" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: b99bc4f1f70787f694b73bc6fce238d4d6cc822c9b31ba8ef1578b180b6f77bc + url: "https://pub.dev" + source: hosted + version: "4.4.8" collection: dependency: transitive description: @@ -49,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + delightful_toast: + dependency: "direct main" + description: + name: delightful_toast + sha256: "93d0b9e89a65947e42daa8aafe552596487dbedc15f68d0480654e789e94bc5b" + url: "https://pub.dev" + source: hosted + version: "1.1.0" fake_async: dependency: transitive description: @@ -57,11 +97,67 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "10cd3f00a247f33b0a5c77574011a87379432bf3fec77a500b55f2bcc30ddd8b" + url: "https://pub.dev" + source: hosted + version: "5.5.4" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "2d15872a8899b0459fab6b4c148fd142e135acfc8a303d383d80b455e4dba7bd" + url: "https://pub.dev" + source: hosted + version: "7.6.3" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: efba45393050ca03d992eae1d305d5fc8c0c9f5980624053512e935c23767c4f + url: "https://pub.dev" + source: hosted + version: "5.14.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "8cfe3c900512399ce8d50fcc817e5758ff8615eeb6fa5c846a4cc47bbf6353b6" + url: "https://pub.dev" + source: hosted + version: "3.13.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e + url: "https://pub.dev" + source: hosted + version: "2.23.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_lints: dependency: "direct dev" description: @@ -70,11 +166,32 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -139,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -192,6 +317,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +341,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6d3a909..153325c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + firebase_core: ^3.13.1 + firebase_auth: ^5.5.4 + cloud_firestore: ^5.6.8 + delightful_toast: ^1.1.0 dev_dependencies: flutter_test: @@ -58,7 +62,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: + assets: + - assets/images/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..bf6d21a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,15 @@ #include "generated_plugin_registrant.h" +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..b83b40a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore + firebase_auth + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST