diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcc8024 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/test + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Firebase Related +**/google-services.json +**/GoogleService-Info.plist +**/firebase_app_id_file.json +**/firebase_options.dart +.firebaserc +.metadata +.firebase/hosting.LmZpcmViYXNlXHNwb29uc2hhcmUtbWVhbHNcaG9zdGluZw.cache +.firebase/spoonshare-meals/hosting/.last_build_id +.firebase/ +ios/Flutter/Debug.xcconfig +ios/Flutter/Release.xcconfig +ios/Runner.xcodeproj/project.pbxproj +ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +.env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89e977c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Shubham Pitekar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..44a7158 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ + +# SpoonShare 🥣 + +**Problem Statement**: Inadequate surplus food distribution generates hunger, necessitating a comprehensive solution. Our project addresses this challenge through an innovative platform, connecting donors with recipients to bridge the gap in food distribution. + +A **Google Solution Challenge Project'24** Organised By Google Developer Student Clubs Project by **Team Innovision Squad From Deogiri Institute of Engineering And Management Studies Chh. Sambhajinagar.** + +## + +## Live Preview + +Here you can view the deployed version +[SpoonShare](https://spoonshare-meals.web.app/) + + +# SpoonShare Project + +## Intro To SpoonShare Video + +[![Intro To SpoonShare](https://github.com/shuence/SpoonShare/assets/65482186/ff3a926f-f796-4609-8502-6b5948efcbd5) +](https://www.youtube.com/watch?v=IKbxF7SYE3Q&ab_channel=SanikaChavan) + + +Short but detailed introduction to SpoonShare. Click on the image above to watch the video. + + +## Setup + +To Setup this project run + +```bash +git clone https://github.com/shuence/SpoonShare +cd SpoonShare +flutter pub get +flutter run +``` + +## Resources + +- [Flutter Docs](https://docs.flutter.dev/) +- [Figma](https://help.figma.com/hc/en-us) +- [Firebase Docs](https://firebase.google.com/docs) + +# Screenshots +
+         
+
+# SpoonShare Features + +- **Surplus Food Map:** Visualize and navigate locations with surplus food, ensuring efficient distribution. +- **User-Friendly Interface:** Intuitive design for seamless interaction for both donors and recipients. +- **Real-Time Updates:** Keep users informed with instant notifications on surplus food availability and distribution. +- **Multi-Language Support:** Ensure inclusivity by providing support for multiple languages. +- **Integration with Social Media:** Facilitate broader outreach and engagement through social media integration. +- **Gamification Elements:** Incentivize frequent donors with rewards and gamified features. +- **Educational Resources:** Offer information on sustainable practices and the impact of food wastage. +- **Volunteer Matching:** Connect volunteers with surplus food distribution opportunities based on their preferences and availability. +- **Donor Recognition:** Acknowledge and appreciate donors for their contributions through a recognition system. +- **Quality and Safety Standards Verification:** Establish and enforce guidelines to ensure the quality and safety of donated food. +- **Feedback and Ratings System:** Promote transparency and accountability through user feedback and ratings. +- **In-App Challenges and Campaigns:** Engage users with interactive challenges and campaigns to encourage participation. +- **Collaboration with Local Governments and NGO'S:** Foster partnerships with local authorities to streamline operations and adhere to regulations. +## Tech Stack +**Technologies involved/used:** +- **Flutter:** Google's UI toolkit for cross-platform app development. +- **Firebase:** Google's platform for authentication, database, and cloud services. +- **Google Maps API:** Integrates dynamic maps and location-based services. +- **NLP tools:** Enables text analysis and language understanding. +- **Google Cloud:** Offers scalable cloud services and machine learning. +- **Android Studio:** Official IDE for Android development. +- **Web (HTML, CSS, JS):** Standard web technologies for UI. +- **Google Maps:** Web mapping service for interactive maps. +- **Google API:** Collection of APIs for diverse services. +- **Google Analytics:** Tracks and reports website/app traffic. +- **Google Sign-In:** Authentication using Google credentials. +- **Google Speech API:** Integrates speech recognition capabilities. +# SpoonShare Project Implementation Overview + +### Technology Stack +- Flutter: Cross-platform app development. +- Firebase: Real-time updates, user authentication, and data storage. +- Google Maps API: Efficient navigation. + +### User Interface (UI) Design +- Figma: Collaborative UI/UX design. +- User-friendly interface with clear "Donate Food" and "Find Food" buttons. + +### Chatbot Integration +- Dialogflow: Interactive chatbot functionality. +- Friendly and supportive chatbot tone for enhanced engagement. + +### Gamification Elements +- Flutter: Implementation of gamification features. +- Rewards for frequent donors to encourage sustained engagement. + +### Multi-Language Support +- Flutter's localization tools for supporting multiple languages. + +### Educational Resources +- Collaboration with NGOs to provide educational content on food waste. + +### Volunteer Matching +- Feature to connect willing volunteers with NGOs and events. + +### Quality and Safety Standards Verification +- Establishment of guidelines for donor verification. + +### Feedback and Ratings System +- System to maintain transparency and encourage user participation. + +### In-App Challenges and Campaigns +- Engaging challenges and campaigns for sustained user interest. + +### Collaboration with Local Governments +- Partnerships with local governments for legal compliance. + +### Marketing and Awareness +- Utilization of social media platforms for promotional campaigns. +- Collaboration with influencers and organizations for a wider reach. + +### Post-Launch Optimization +- Regular analysis of user data for improvements and enhancements. +- Community feedback encouraged for continuous improvement. + +### Community Building and Partnerships +- Robust community engagement strategy for user interaction. +- Partnerships with NGOs, local businesses, and institutions for expanded impact. + +# Hi, We are InnovisionSquad! 👋 + + +## 🚀 About us + +We are a team from Deogiri Institute of Engineering And Management Studies Chh. Sambhajinagar and Core Team Members of [GDSC DIEMS](https://gdsc.community.dev/deogiri-institute-of-engineering-and-management-studies-aurangabad/) + +- Sanika Chavan - [Sanika](https://linkedin.com/in/sanika-chavan-52457b236/) +- Krishna Dnyaneshwar Aute - [Krishna](https://www.linkedin.com/in/krishna-aute-195b2b135/) +- Shubham Vishnu Pitekar - [Shuence](https://shuence.com) +- Mohammed Rehan - [Rehan](https://www.linkedin.com/in/mdrehan15/) + +## Happy coding 💯 + +Made with love from [InnovsionSquad]() ❤️ diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..faa564e --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,81 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.spoonsharemeals" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.spoonsharemeals.spoonsharemeals" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + multiDexEnabled true + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.android.support:multidex:1.0.3' + implementation platform('com.google.firebase:firebase-bom:32.7.1') + implementation("com.google.firebase:firebase-crashlytics") + implementation("com.google.firebase:firebase-perf") + implementation ('com.google.firebase:firebase-analytics') + implementation ('com.google.firebase:firebase-auth') + implementation ('com.google.firebase:firebase-firestore') + implementation ('com.google.firebase:firebase-storage') + implementation ('com.google.firebase:firebase-database') + implementation ('com.google.firebase:firebase-messaging') +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7765428 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/spoonsharemeals/MainActivity.kt b/android/app/src/main/kotlin/com/example/spoonsharemeals/MainActivity.kt new file mode 100644 index 0000000..e257a51 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/spoonsharemeals/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.spoonsharemeals + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/kotlin/com/example/spoonsharemeals/spoonsharemeals/MainActivity.kt b/android/app/src/main/kotlin/com/example/spoonsharemeals/spoonsharemeals/MainActivity.kt new file mode 100644 index 0000000..458a24a --- /dev/null +++ b/android/app/src/main/kotlin/com/example/spoonsharemeals/spoonsharemeals/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.spoonsharemeals.spoonsharemeals + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..289bd86 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..1d4a454 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..98f087b Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..e304c5a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..d443fae Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..6241e7e --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.3.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22" + classpath 'com.google.gms:google-services:4.4.0' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..7cd7128 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,29 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false +} + +include ":app" diff --git a/assets/images/google.png b/assets/images/google.png new file mode 100644 index 0000000..221d6cb Binary files /dev/null and b/assets/images/google.png differ diff --git a/assets/images/mail.png b/assets/images/mail.png new file mode 100644 index 0000000..ed1b52a Binary files /dev/null and b/assets/images/mail.png differ diff --git a/assets/images/onboarding.png b/assets/images/onboarding.png new file mode 100644 index 0000000..3e9de56 Binary files /dev/null and b/assets/images/onboarding.png differ diff --git a/assets/images/playstore.png b/assets/images/playstore.png new file mode 100644 index 0000000..6b5fc79 Binary files /dev/null and b/assets/images/playstore.png differ diff --git a/assets/images/spoonshare_launcher.png b/assets/images/spoonshare_launcher.png new file mode 100644 index 0000000..a98f3d5 Binary files /dev/null and b/assets/images/spoonshare_launcher.png differ diff --git a/assets/images/thankyou.jpg b/assets/images/thankyou.jpg new file mode 100644 index 0000000..f424c2c Binary files /dev/null and b/assets/images/thankyou.jpg differ diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..e912a28 --- /dev/null +++ b/firebase.json @@ -0,0 +1,13 @@ +{ + "hosting": { + "source": ".", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "frameworksBackend": { + "region": "us-central1" + } + } +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..3d1b0cc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..4c39fe6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..8bc0b7e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..d5378c2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..90f98b6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..ee917f6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..fc9f31f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..8bc0b7e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..7e97deb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..5c21eef Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..dc9bb0b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..4e3b471 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..8d7c624 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..b3f05ab Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..5c21eef Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..8c6f124 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..289bd86 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..e304c5a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..aae4184 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..fbad615 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..780a0c2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..6f94d9e --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Spoonsharemeals + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + spoonsharemeals + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/controllers/auth/signin_controller.dart b/lib/controllers/auth/signin_controller.dart new file mode 100644 index 0000000..6826c8b --- /dev/null +++ b/lib/controllers/auth/signin_controller.dart @@ -0,0 +1,190 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/home/home.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class SignInController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final GoogleSignIn _googleSignIn = GoogleSignIn(); + + Future signIn({ + required String email, + required String password, + BuildContext? context, + }) async { + if (!_isValidInputEmail(email, context) || + !_isValidInputPassword(password, context)) { + return; + } + + try { + // Sign in with email and password + UserCredential userCredential = await _auth.signInWithEmailAndPassword( + email: email, password: password); + + // Check if email is verified + + /* if (userCredential.user!.emailVerified) { + _showSuccessSnackbar(context, 'Signup successful'); + // Navigate to the desired screen (e.g., Onboarding) + + } + */ +//else { + + /* If email is not verified, provide an option to resend the verification email + _showEmailVerificationDialog(context); + }*/ + // Load user details and save locally + await _loadAndSaveUserLocally(userCredential.user!); + + // Show success message + _showSuccessSnackbar(context, 'Signin successful'); + } catch (e) { + // Show error message + _showErrorSnackbar(context, "Error: $e"); + } + } + + Future signInWithGoogle(BuildContext? context) async { + try { + // Trigger Google Sign In + final GoogleSignInAccount? googleSignInAccount = + await _googleSignIn.signIn(); + + if (googleSignInAccount == null) { + // Google sign-in canceled + return; + } + + final GoogleSignInAuthentication googleSignInAuthentication = + await googleSignInAccount.authentication; + + // Sign in to Firebase with Google credentials + final OAuthCredential googleAuthCredential = + GoogleAuthProvider.credential( + accessToken: googleSignInAuthentication.accessToken, + idToken: googleSignInAuthentication.idToken, + ); + + UserCredential userCredential = + await _auth.signInWithCredential(googleAuthCredential); + + // Load and save user details + await _loadAndSaveUserLocally(userCredential.user!); + + // Show success message + _showSuccessSnackbar(context, 'Google Signin successful'); + + // You can navigate to the desired screen after successful signup + Navigator.pushReplacement( + context!, + MaterialPageRoute( + builder: (context) => const HomeScreen(), + ), + ); + } catch (e) { + // Show error message + _showErrorSnackbar(context, "Error: $e"); + } + } + +Future _loadAndSaveUserLocally(User user) async { + try { + DocumentSnapshot userDoc = + await _firestore.collection('users').doc(user.uid).get(); + + if (userDoc.exists) { + String fullName = userDoc['fullName']; + String contactNumber = userDoc['contactNumber']; + String profileImageUrl = userDoc['profileImageUrl']; + String role = userDoc['role']; + String organisation = userDoc['organisation']; + + await _saveUserLocally(fullName, user.email!, contactNumber, role, profileImageUrl, organisation); + } else { + throw Exception("User document does not exist"); + } + } catch (e) { + print("Error loading user profile: $e"); + } +} + + Future _saveUserLocally( + String fullName, String email, String contactNumber, String role, String ProfileImageUrl, String organisation) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('fullName', fullName); + prefs.setString('email', email); + prefs.setString('contactNumber', contactNumber); + prefs.setString('profileImageUrl', ProfileImageUrl); + prefs.setString("role", role); + prefs.setString("organisation", organisation); + } + + void _showEmailVerificationDialog(BuildContext? context) { + // Show a dialog with an option to resend the verification email + showDialog( + context: context!, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Email Verification'), + content: const Text( + 'Please verify your email by clicking the verification link sent to your email address. If you haven\'t received the email, you can resend it.', + ), + actions: [ + TextButton( + onPressed: () async { + // Resend verification email + await _auth.currentUser!.sendEmailVerification(); + Navigator.pop(context); + _showSuccessSnackbar(context, 'Verification email resent'); + }, + child: const Text('Resend Email'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + bool _isValidInputEmail(String email, BuildContext? context) { + if (!RegExp(r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$') + .hasMatch(email)) { + _showErrorSnackbar(context, 'Invalid Email'); + return false; + } + return true; + } + + bool _isValidInputPassword(String password, BuildContext? context) { + if (password.isEmpty || password.length < 6) { + _showErrorSnackbar(context, 'Invalid Password'); + return false; + } + return true; + } + + void _showErrorSnackbar(BuildContext? context, String message) { + if (context != null) { + showErrorSnackbar(context, message); + } + } + + void _showSuccessSnackbar(BuildContext? context, String message) { + if (context != null) { + showSuccessSnackbar(context, message); + } + } +} diff --git a/lib/controllers/auth/signup_controller.dart b/lib/controllers/auth/signup_controller.dart new file mode 100644 index 0000000..1a656f5 --- /dev/null +++ b/lib/controllers/auth/signup_controller.dart @@ -0,0 +1,244 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/home/home.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class SignUpController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final GoogleSignIn _googleSignIn = GoogleSignIn(); + + Future signUp({ + required String fullName, + required String email, + required String contactNumber, + required String password, + required String confirmPassword, + String? profileImageUrl, + String? role, + String? organisation, + BuildContext? context, + }) async { + if (!_isValidInput(fullName, 'Full Name', context) || + !_isValidInputEmail(email, context) || + !_isValidInputContactNumber(contactNumber, context) || + !_isValidInputPassword(password, context) || + !_arePasswordsMatching(password, confirmPassword, context)) { + return; + } + + try { + // Create user with email and password + UserCredential userCredential = + await _auth.createUserWithEmailAndPassword( + email: email, + password: password, + ); + + // Save user details to Firestore + if (userCredential.user != null) { + // Save user details to Firestore + await _firestore.collection('users').doc(userCredential.user!.uid).set({ + 'fullName': fullName, + 'email': email, + 'contactNumber': contactNumber, + 'profileImageUrl': profileImageUrl ?? + "https://github.com/shuence/AdventureSquad/blob/main/user.png", + 'role': role ?? "Individual", + 'organisation': organisation ?? "", + }); + + // Save user details locally using SharedPreferences + await _saveUserLocally(fullName, email, contactNumber, + profileImageUrl ?? "", role ?? "Individual", organisation ?? ""); + + // Show success message + _showSuccessSnackbar(context, 'Signup successful'); + + // Send email verification + await userCredential.user!.sendEmailVerification(); + } else { + // Handle the case where userCredential.user is null + _showErrorSnackbar(context, 'Error: User data is null'); + } + } catch (e) { + // Show error message + _showErrorSnackbar(context, "Error: $e"); + } + } + + Future signUpWithGoogle(BuildContext? context) async { + try { + // Trigger Google Sign In + final GoogleSignInAccount? googleSignInAccount = + await _googleSignIn.signIn(); + + if (googleSignInAccount == null) { + // Google sign-in canceled + return; + } + + final GoogleSignInAuthentication googleSignInAuthentication = + await googleSignInAccount.authentication; + + // Sign in to Firebase with Google credentials + final OAuthCredential googleAuthCredential = + GoogleAuthProvider.credential( + accessToken: googleSignInAuthentication.accessToken, + idToken: googleSignInAuthentication.idToken, + ); + + UserCredential userCredential = + await _auth.signInWithCredential(googleAuthCredential); + + // Fetch additional details from Google Sign-In + String fullName = userCredential.user?.displayName ?? ''; + String email = userCredential.user?.email ?? ''; + String contactNumber = ''; + String profileImageUrl = userCredential.user?.photoURL ?? ''; + String? role; + String? organisation; + + // Save user details to Firestore (modify as needed) + await _firestore.collection('users').doc(userCredential.user?.uid).set({ + 'fullName': fullName, + 'email': email, + 'contactNumber': contactNumber, + 'profileImageUrl': profileImageUrl, + 'role': role ?? "Individual", + 'organisation': organisation ?? "", + }); + await _saveUserLocally(fullName, email, contactNumber, profileImageUrl, + role ?? "Individual", organisation ?? ""); + + // Show success message + _showSuccessSnackbar(context, 'Google Signup successful'); + + Navigator.pushReplacement( + context!, + MaterialPageRoute( + builder: (context) => const HomeScreen(), + ), + ); + } catch (e) { + // Show error message + _showErrorSnackbar(context, "Error: $e"); + } + } + + Future _saveUserLocally( + String fullName, + String email, + String contactNumber, + String profileImageUrl, + String role, + String organisation) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('fullName', fullName); + prefs.setString('email', email); + prefs.setString('contactNumber', contactNumber); + prefs.setString('profileImageUrl', profileImageUrl); + prefs.setString('role', role); + prefs.setString('organisation', organisation); + } + + void _showEmailVerificationDialog(BuildContext? context) { + // Show a dialog with an option to resend the verification email + showDialog( + context: context!, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Email Verification'), + content: const Text( + 'Please verify your email by clicking the verification link sent to your email address. If you haven\'t received the email, you can resend it.', + ), + actions: [ + TextButton( + onPressed: () async { + // Resend verification email + await _auth.currentUser!.sendEmailVerification(); + Navigator.pop(context); + _showSuccessSnackbar(context, 'Verification email resent'); + }, + child: const Text('Resend Email'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + bool _isValidInput(String value, String field, BuildContext? context) { + if (value.isEmpty) { + _showErrorSnackbar(context, 'Invalid $field'); + return false; + } + return true; + } + + bool _isValidInputEmail(String email, BuildContext? context) { + if (!RegExp(r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$') + .hasMatch(email)) { + _showErrorSnackbar(context, 'Invalid Email'); + return false; + } + return true; + } + + bool _isValidInputContactNumber(String contactNumber, BuildContext? context) { + if (contactNumber.isEmpty || + contactNumber.length != 10 || + !isNumeric(contactNumber)) { + _showErrorSnackbar(context, 'Invalid Contact Number'); + return false; + } + return true; + } + + bool isNumeric(String? str) { + if (str == null) { + return false; + } + return int.tryParse(str) != null; + } + + bool _isValidInputPassword(String password, BuildContext? context) { + if (password.isEmpty || password.length < 6) { + _showErrorSnackbar(context, 'Invalid Password'); + return false; + } + return true; + } + + bool _arePasswordsMatching( + String password, String confirmPassword, BuildContext? context) { + if (password != confirmPassword) { + _showErrorSnackbar(context, 'Passwords do not match'); + return false; + } + return true; + } + + void _showErrorSnackbar(BuildContext? context, String message) { + if (context != null) { + showErrorSnackbar(context, message); + } + } + + void _showSuccessSnackbar(BuildContext? context, String message) { + if (context != null) { + showSuccessSnackbar(context, message); + } + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..5f73865 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:spoonshare/firebase_options.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spoonshare/splash_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + await SharedPreferences.getInstance(); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Spoon Share', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const SplashScreen(), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/lib/models/foodcards/foodcards.dart b/lib/models/foodcards/foodcards.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/models/users/user.dart b/lib/models/users/user.dart new file mode 100644 index 0000000..584e98f --- /dev/null +++ b/lib/models/users/user.dart @@ -0,0 +1,95 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class UserProfile { + late String userId = FirebaseAuth.instance.currentUser!.uid; + late String fullName = ''; + late String email = ''; + late String contactNumber = ''; + late String profileImageUrl = ''; + late String role = ''; + late String organisation = ''; + + static final UserProfile _instance = UserProfile._internal(); + + factory UserProfile() { + return _instance; + } + + UserProfile._internal() { + loadUserProfile(); + } + + Future loadUserProfile() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + userId = prefs.getString('userId') ?? ''; + fullName = prefs.getString('fullName') ?? ''; + email = prefs.getString('email') ?? ''; + contactNumber = prefs.getString('contactNumber') ?? ''; + + // Fetch role and organisation from Firestore + await loadUserDataFromFirestore(); + + // Save profileImageUrl to instance variable + profileImageUrl = prefs.getString('profileImageUrl') ?? ''; + + // Rest of the method remains unchanged + } + + Future loadUserDataFromFirestore() async { + try { + CollectionReference users = + FirebaseFirestore.instance.collection('users'); + String userId = FirebaseAuth.instance.currentUser!.uid; + + DocumentSnapshot userDoc = await users.doc(userId).get(); + + role = userDoc['role']; + organisation = userDoc['organisation']; + + // Save role and organisation to SharedPreferences + saveRoleAndOrganisationToSharedPreferences(); + } catch (e) { + print('Error loading user data from Firestore: $e'); + } + } + + void saveRoleAndOrganisationToSharedPreferences() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('role', role); + prefs.setString('organisation', organisation); + } + + bool isAuthenticated() { + return email.isNotEmpty; + } + + String getFullName() { + return fullName; + } + + String getEmail() { + return email; + } + + String getContactNumber() { + return contactNumber; + } + + String getUserId() { + return userId; + } + + String getProfileImageUrl() { + return profileImageUrl; + } + + String getRole() { + return role; + } + + String getOrganisation() { + return organisation; + } +} diff --git a/lib/screens/auth/email_signup.dart b/lib/screens/auth/email_signup.dart new file mode 100644 index 0000000..0e6e48a --- /dev/null +++ b/lib/screens/auth/email_signup.dart @@ -0,0 +1,406 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/controllers/auth/signup_controller.dart'; +import 'package:spoonshare/screens/auth/signin.dart'; +import 'package:spoonshare/screens/home/home.dart'; +import 'package:spoonshare/widgets/loader.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; + +class EmailSignUpScreen extends StatefulWidget { + const EmailSignUpScreen({Key? key}) : super(key: key); + + @override + _EmailSignUpScreenState createState() => _EmailSignUpScreenState(); +} + +class _EmailSignUpScreenState extends State { + final SignUpController _signUpController = SignUpController(); + final TextEditingController _fullNameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _contactNumberController = + TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + bool _isPasswordVisible = false; + bool _isConfirmPasswordVisible = false; + + @override + void dispose() { + _fullNameController.dispose(); + _emailController.dispose(); + _contactNumberController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Center( + child: Container( + width: 360, + height: MediaQuery.of(context).size.height + 100, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(color: Colors.white), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 168, + height: 32, + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.86), + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Text( + 'MEALS OF GRACE BY', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + )), + ), + const Text( + 'SpoonShare', + style: TextStyle( + color: Colors.black, + fontSize: 42, + fontFamily: 'Roboto', + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + const Text( + 'अब भूखे नहीं रहेंगे.', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Asar', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + SizedBox( + width: 275, + height: 66, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Welcome To Help!', + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 275, + child: Text( + 'Find Free Food Near You / Donate food by entering details.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black.withOpacity(0.800000011920929), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 32), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InputField( + label: 'Full Name', + controller: _fullNameController, + ), + const SizedBox(height: 16), + InputField(label: 'Email', controller: _emailController), + const SizedBox(height: 16), + InputField( + label: 'Contact Number', + controller: _contactNumberController), + const SizedBox(height: 16), + InputField( + label: 'Password', + isPassword: true, + controller: _passwordController, + isPasswordVisible: _isPasswordVisible, + togglePasswordVisibility: () { + _togglePasswordVisibility(); + }, + ), + const SizedBox(height: 16), + InputField( + label: 'Confirm Password', + isPassword: true, + controller: _confirmPasswordController, + isPasswordVisible: _isConfirmPasswordVisible, + togglePasswordVisibility: () { + _toggleConfirmPasswordVisibility(); + }, + ), + ], + ), + const SizedBox(height: 16), + Container( + width: 309, + height: 32, + child: const Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'By ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Registering', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ' or ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Login', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ' you have agreed to these ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Terms and Conditions.', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + Container( + margin: const EdgeInsets.only( + top: 20.0), // Adjust the top margin as needed + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const SignInScreen()), // Replace LoginScreen with the actual screen you want to navigate to + ); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Already have an account?', + style: TextStyle( + color: + Colors.black.withOpacity(0.699999988079071), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: ' ', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: 'Log in', + style: TextStyle( + color: Color(0xFF0081DF), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + decoration: TextDecoration.underline, + height: 0, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () async { + // Show loading indicator + showLoadingDialog(context); + + try { + // Perform signup asynchronously + await _signUpController.signUp( + fullName: _fullNameController.text, + email: _emailController.text, + contactNumber: _contactNumberController.text, + password: _passwordController.text, + confirmPassword: _confirmPasswordController.text, + context: context, + ); + + // Hide loading indicator + Navigator.of(context).pop(); // Pop the loading dialog + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen()), + ); + } catch (e) { + // Handle any exceptions during signup + print("Signup failed: $e"); + + // Hide loading indicator + Navigator.of(context).pop(); // Pop the loading dialog + + // You can customize this part based on your requirements + showErrorSnackbar( + context, 'Signup failed. Please try again.'); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const SizedBox( + width: 312, + height: 45, + child: Center( + child: Text( + 'Create Account', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + letterSpacing: 0.36, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _togglePasswordVisibility() { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + } + + void _toggleConfirmPasswordVisibility() { + setState(() { + _isConfirmPasswordVisible = !_isConfirmPasswordVisible; + }); + } +} + +class InputField extends StatelessWidget { + final String label; + final bool isPassword; + final TextEditingController controller; + final bool? isPasswordVisible; + final VoidCallback? togglePasswordVisibility; + + const InputField({ + Key? key, + required this.label, + this.isPassword = false, + required this.controller, + this.isPasswordVisible, + this.togglePasswordVisibility, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, // Ensure the controller is assigned here + obscureText: isPassword && isPasswordVisible != true, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + suffixIcon: isPassword && togglePasswordVisibility != null + ? IconButton( + icon: Icon( + isPasswordVisible! ? Icons.visibility : Icons.visibility_off, + ), + onPressed: togglePasswordVisibility, + ) + : null, + ), + ); + } +} diff --git a/lib/screens/auth/forgot_password.dart b/lib/screens/auth/forgot_password.dart new file mode 100644 index 0000000..01a78e0 --- /dev/null +++ b/lib/screens/auth/forgot_password.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/auth/signin.dart'; +import 'package:spoonshare/services/forgot_password.dart'; +import 'package:spoonshare/widgets/loader.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; + +class ForgotPasswordScreen extends StatelessWidget { + final ForgotPasswordService _forgotPasswordService = ForgotPasswordService(); + final TextEditingController _emailController = TextEditingController(); + + ForgotPasswordScreen({Key? key}) : super(key: key); + + void _onSubmitClick(BuildContext context) async { + String email = _emailController.text.trim(); + + if (email.isEmpty) { + showErrorSnackbar(context, 'Please enter your email.'); + return; + } + RegExp emailRegex = RegExp( + r'^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$', + ); + + if (!emailRegex.hasMatch(email)) { + showErrorSnackbar(context, 'Please enter a valid email.'); + return; + } + + try { + showLoadingDialog(context); + await _forgotPasswordService.resetPassword(email); + showSuccessSnackbar(context, 'Password reset email sent successfully.'); + } catch (e) { + showErrorSnackbar(context, ('Error: $e')); + } finally { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + width: MediaQuery.of(context).size.width - 40, + margin: const EdgeInsets.all(40.0), + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(color: Colors.white), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Reset your password', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + height: 1.2, + letterSpacing: -0.53, + ), + ), + const SizedBox(height: 20), + const Text( + 'Enter your registered email below', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 1.2, + letterSpacing: -0.29, + ), + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: 55, + margin: const EdgeInsets.only(bottom: 20.0), + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1), + borderRadius: BorderRadius.all(Radius.circular(100)), + ), + ), + child: TextFormField( + controller: _emailController, + decoration: const InputDecoration( + hintText: 'Email', + contentPadding: EdgeInsets.symmetric(horizontal: 20), + border: InputBorder.none, + ), + ), + ), + Container( + width: double.infinity, + height: 57, + decoration: const ShapeDecoration( + color: Color(0xFFFBDE3F), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(100)), + ), + ), + child: ElevatedButton( + onPressed: () => _onSubmitClick(context), + style: ElevatedButton.styleFrom( + backgroundColor: + const Color(0xFFFBDE3F), // Background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100), + ), + ), + child: const Center( + child: Text( + 'Submit', + style: TextStyle( + color: Colors.black, + fontSize: 17, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + height: 1.2, + letterSpacing: -0.37, + ), + ), + ), + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 215, + child: GestureDetector( + onTap: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const SignInScreen()), + ); + }, + child: const Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Remember the password? ', + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 1.2, + letterSpacing: -0.29, + ), + ), + TextSpan( + text: 'Login', + style: TextStyle( + color: Colors.blue, + fontSize: 15, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + height: 1.2, + letterSpacing: -0.29, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart new file mode 100644 index 0000000..ad875d1 --- /dev/null +++ b/lib/screens/auth/signin.dart @@ -0,0 +1,461 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/controllers/auth/signin_controller.dart'; +import 'package:spoonshare/screens/auth/forgot_password.dart'; +import 'package:spoonshare/screens/auth/signup.dart'; +import 'package:spoonshare/screens/home/home.dart'; +import 'package:spoonshare/widgets/loader.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; + +class SignInScreen extends StatefulWidget { + const SignInScreen({super.key}); + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final SignInController _signInController = SignInController(); + final TextEditingController _emailController = TextEditingController(); + + final TextEditingController _passwordController = TextEditingController(); + bool _isPasswordVisible = false; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Center( + child: Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(top: 40), + constraints: const BoxConstraints( + maxWidth: 600, // Adjust the maximum width as needed + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 168, + height: 32, + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.86), + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Text( + 'MEALS OF GRACE BY', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + )), + ), + const Text( + 'SpoonShare', + style: TextStyle( + color: Colors.black, + fontSize: 42, + fontFamily: 'Roboto', + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + const Text( + 'अब भूखे नहीं रहेंगे.', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Asar', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + SizedBox( + width: 275, + height: 66, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Welcome To Help!', + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 275, + child: Text( + 'Find Free Food Near You / Donate food by entering details.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black.withOpacity(0.800000011920929), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 32), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 16), + InputField(label: 'Email', controller: _emailController), + const SizedBox(height: 16), + const SizedBox(height: 16), + InputField( + label: 'Password', + isPassword: true, + controller: _passwordController, + isPasswordVisible: _isPasswordVisible, + togglePasswordVisibility: () { + _togglePasswordVisibility(); + }, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => ForgotPasswordScreen()), + ); + }, + child: const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(top: 8.0, right: 8.0), + child: Text( + 'forgot password?', + style: TextStyle( + color: Colors.black, + fontSize: 15, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + textBaseline: TextBaseline.alphabetic, + height: 0, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + const SizedBox( + width: 309, + height: 32, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'By ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Registering', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ' or ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Login', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ' you have agreed to these ', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: 'Terms and Conditions.', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + Container( + margin: const EdgeInsets.only( + top: 20.0), // Adjust the top margin as needed + child: Container( + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SignUpScreen()), + ); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Don\'t have an account?', + style: TextStyle( + color: + Colors.black.withOpacity(0.699999988079071), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: ' ', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: 'Sign Up', + style: TextStyle( + color: Color(0xFF0081DF), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + decoration: TextDecoration.underline, + height: 0, + ), + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () async { + // Show loading indicator + showLoadingDialog(context); + + try { + // Perform signup asynchronously + await _signInController.signIn( + email: _emailController.text, + password: _passwordController.text, + context: context, + ); + // Hide loading indicator + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen()), + ); + } catch (e) { + // Handle any exceptions during signup + print("Signup failed: $e"); + // Hide loading indicator + // You can customize this part based on your requirements + showErrorSnackbar( + context, 'Signin failed. Please try again.'); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const SizedBox( + width: 312, + height: 45, + child: Center( + child: Text( + 'Log In', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + letterSpacing: 0.36, + ), + ), + ), + ), + ), + + const SizedBox(height: 16), + const Center( + child: Column( + children: [ + Text( + 'Or', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 4, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), // Adjust the top padding as needed + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 41.50, vertical: 12), + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.07999999821186066), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: InkWell( + onTap: () { + _signInController.signInWithGoogle(context); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/images/google.png"), + fit: BoxFit.fill, + ), + ), + ), + const SizedBox(width: 12), + const Text( + 'Sign In with Google', + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _togglePasswordVisibility() { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + } +} + +class InputField extends StatelessWidget { + final String label; + final bool isPassword; + final TextEditingController controller; + final bool? isPasswordVisible; + final VoidCallback? togglePasswordVisibility; + + const InputField({ + Key? key, + required this.label, + this.isPassword = false, + required this.controller, + this.isPasswordVisible, + this.togglePasswordVisibility, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, // Ensure the controller is assigned here + obscureText: isPassword && isPasswordVisible != true, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + suffixIcon: isPassword && togglePasswordVisibility != null + ? IconButton( + icon: Icon( + isPasswordVisible! ? Icons.visibility : Icons.visibility_off, + ), + onPressed: togglePasswordVisibility, + ) + : null, + ), + ); + } +} diff --git a/lib/screens/auth/signup.dart b/lib/screens/auth/signup.dart new file mode 100644 index 0000000..a876022 --- /dev/null +++ b/lib/screens/auth/signup.dart @@ -0,0 +1,297 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/controllers/auth/signup_controller.dart'; +import 'package:spoonshare/screens/auth/email_signup.dart'; +import 'package:spoonshare/screens/auth/signin.dart'; + +class SignUpScreen extends StatelessWidget { + final SignUpController signUpController = SignUpController(); + + SignUpScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + width: 360, + height: 800, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(color: Colors.white), + child: Stack( + children: [ + Positioned( + left: 65, + top: 65, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.8600000143051147), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'MEALS OF GRACE BY', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + const Text( + 'SpoonShare', + style: TextStyle( + color: Colors.black, + fontSize: 42, + fontFamily: 'Roboto', + fontWeight: FontWeight.w800, + height: 0, + ), + ), + const SizedBox(height: 4), + const Text( + 'अब भूखे नहीं रहेंगे.', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Asar', + fontWeight: FontWeight.w400, + height: 0, + ), + ), + ], + ), + ), + Positioned( + left: 43, + top: 224, + child: Container( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Welcome To Help!', + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 275, + child: Text( + 'Find Free Food Near You / Donate food by entering details.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black.withOpacity(0.800000011920929), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + ), + ], + ), + ), + ), + Positioned( + left: 65, + top: 570, + child: GestureDetector( + onTap: () { + // Navigate to the login screen when tapped + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const SignInScreen()), // Replace LoginScreen with the actual screen you want to navigate to + ); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Already have an account?', + style: TextStyle( + color: Colors.black.withOpacity(0.699999988079071), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: ' ', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: 'Log in', + style: TextStyle( + color: Color(0xFF0081DF), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + decoration: TextDecoration.underline, + height: 0, + ), + ), + ], + ), + ), + ), + ), + const Positioned( + left: 43, + top: 362, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Continue with', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + Positioned( + left: 39, + top: 419, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 48, vertical: 12), + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.07999999821186066), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: InkWell( + onTap: () { + // Navigate to another screen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EmailSignUpScreen()), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/images/mail.png"), + fit: BoxFit.fill, + ), + ), + ), + const SizedBox(width: 12), + const Text( + 'Sign up with email', + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + )), + Positioned( + left: 39, + top: 491, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 41.50, vertical: 12), + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.07999999821186066), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: InkWell( + onTap: () { + signUpController.signUpWithGoogle(context); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/images/google.png"), + fit: BoxFit.fill, + ), + ), + ), + const SizedBox(width: 12), + const Text( + 'Sign up with Google', + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + )), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/chat/chat_page.dart b/lib/screens/chat/chat_page.dart new file mode 100644 index 0000000..5e243c4 --- /dev/null +++ b/lib/screens/chat/chat_page.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; + +class ChatPage extends StatelessWidget { + const ChatPage({Key? key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Chat Page"), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ), + body: const Center( + child: Text("Chat Page Content"), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } +} diff --git a/lib/screens/donate/donate_food.dart b/lib/screens/donate/donate_food.dart new file mode 100644 index 0000000..8e8a834 --- /dev/null +++ b/lib/screens/donate/donate_food.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class DonateFoodScreenContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + // Add your DonateScreen content here + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16), + Text( + 'Donate Screen Content', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + // Add more widgets as needed + ], + ); + } +} diff --git a/lib/screens/donate/donate_page.dart b/lib/screens/donate/donate_page.dart new file mode 100644 index 0000000..1e27f15 --- /dev/null +++ b/lib/screens/donate/donate_page.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/donate/donate_food.dart'; +import 'package:spoonshare/screens/donate/share_food.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; + +class DonatePage extends StatefulWidget { + const DonatePage({Key? key}) : super(key: key); + + @override + _DonatePageState createState() => _DonatePageState(); +} + +class _DonatePageState extends State { + bool isShareFoodSelected = true; + bool isDonateFoodSelected = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Donor Page', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Container( + width: 360, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: BorderRadius.circular(15), + ), + ), + ), + const SizedBox(height: 8), + Container( + width: 380, + margin: const EdgeInsets.only(top: 20), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + setState(() { + isShareFoodSelected = true; + isDonateFoodSelected = false; + }); + }, + child: Container( + width: 180, + height: 40, + decoration: ShapeDecoration( + color: isShareFoodSelected ? Colors.black : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: Center( + child: Text( + 'Share Food', + style: TextStyle( + color: isShareFoodSelected ? Colors.white : Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ), + ), + ), + GestureDetector( + onTap: () { + setState(() { + isShareFoodSelected = false; + isDonateFoodSelected = true; + }); + }, + child: Container( + width: 180, + height: 40, + decoration: ShapeDecoration( + color: isDonateFoodSelected ? Colors.black : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: Center( + child: Text( + 'Donate Food', + style: TextStyle( + color: isDonateFoodSelected ? Colors.white : Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ), + ), + ), + ], + ), + ), + // Conditional rendering of ShareFoodScreen content + if (isShareFoodSelected) ...[ + ShareFoodScreenContent(), + ], + // Conditional rendering of DonateScreen content + if (isDonateFoodSelected) ...[ + DonateFoodScreenContent(), + ], + ], + ), + ), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } + +} diff --git a/lib/screens/donate/share_food.dart b/lib/screens/donate/share_food.dart new file mode 100644 index 0000000..0a8c80a --- /dev/null +++ b/lib/screens/donate/share_food.dart @@ -0,0 +1,493 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:spoonshare/models/users/user.dart'; +import 'package:spoonshare/screens/donate/thank_you.dart'; +import 'package:spoonshare/widgets/custom_text_field.dart'; +import 'package:spoonshare/widgets/loader.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; +import 'package:firebase_storage/firebase_storage.dart' as firebase_storage; + +class ShareFoodScreenContent extends StatefulWidget { + @override + _ShareFoodScreenContentState createState() => _ShareFoodScreenContentState(); +} + +class _ShareFoodScreenContentState extends State { + final TextEditingController _imageController = TextEditingController(); + final TextEditingController _venueController = TextEditingController(); + final TextEditingController _addressController = TextEditingController(); + final TextEditingController _communityController = TextEditingController(); + final TextEditingController _dateController = TextEditingController(); + final TextEditingController _timeController = TextEditingController(); + final TextEditingController _toDateController = TextEditingController(); + final TextEditingController _toTimeController = TextEditingController(); + + File? _imageFile; + String _selectedFoodType = ''; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + _buildImageUploadBox(), + const SizedBox(height: 16), + CustomTextField( + label: 'Venue', + controller: _venueController, + ), + const SizedBox(height: 16), + CustomTextField( + label: 'Enter Address', + controller: _addressController, + ), + const SizedBox(height: 16), + CustomTextField( + label: 'For whom it is? (Commuity)', + controller: _communityController, + ), + const SizedBox(height: 16), + _buildDateAndTimeInputs(context), + const SizedBox(height: 16), + _buildDropdownInput(), + const SizedBox(height: 16), + _buildSubmitButton(), + ], + ); + } + +Widget _buildDateAndTimeInputs(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('From Date'), + TextField( + controller: _dateController, + readOnly: true, + onTap: () async { + DateTime? selectedDate = await _selectDate(context, _dateController); + if (selectedDate != null) { + _dateController.text = selectedDate.toLocal().toString().split(' ')[0]; + } + }, + decoration: const InputDecoration( + hintText: 'Select Date', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('From Time'), + TextField( + controller: _timeController, + readOnly: true, + onTap: () async { + TimeOfDay? selectedTime = await _selectTime(context, _timeController); + if (selectedTime != null) { + _timeController.text = selectedTime.format(context); + } + }, + decoration: const InputDecoration( + hintText: 'Select Time', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('To Date'), + TextField( + controller: _toDateController, + readOnly: true, + onTap: () async { + DateTime? selectedDate = await _selectDate(context, _toDateController); + if (selectedDate != null) { + _toDateController.text = selectedDate.toLocal().toString().split(' ')[0]; + } + }, + decoration: const InputDecoration( + hintText: 'Select Date', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('To Time'), + TextField( + controller: _toTimeController, + readOnly: true, + onTap: () async { + TimeOfDay? selectedTime = await _selectTime(context, _toTimeController); + if (selectedTime != null) { + _toTimeController.text = selectedTime.format(context); + } + }, + decoration: const InputDecoration( + hintText: 'Select Time', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + ], + ), + ], + ); +} + +Future _selectDate( + BuildContext context, TextEditingController controller) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + + if (picked != null) { + controller.text = picked.toLocal().toString().split(' ')[0]; + } + + return picked; +} + +Future _selectTime( + BuildContext context, TextEditingController controller) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + + if (picked != null) { + controller.text = picked.format(context); + } + + return picked; +} + + Widget _buildImageUploadBox() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () { + pickFile(); + }, + child: Container( + width: 280, + height: 180, + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.07999999821186066), + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: Colors.black.withOpacity(0.6000000238418579), + ), + borderRadius: BorderRadius.circular(10), + ), + ), + child: _imageFile == null + ? const Icon( + Icons.camera_alt, + size: 48, + color: Colors.grey, + ) + : Image.file( + _imageFile!, + width: 48, // Adjust the size as needed + height: 48, // Adjust the size as needed + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + '* Fill below details to share food', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ), + ); + } + + Future pickFile() async { + final androidInfo = await DeviceInfoPlugin().androidInfo; + late final Map status; + if (androidInfo.version.sdkInt <= 32) { + status = await [ + Permission.storage, + Permission + .camera, // Request camera permission for devices with SDK <= 32 + ].request(); + } else { + status = await [ + Permission.photos, + Permission.camera, + Permission.notification, + ].request(); + } + + var allAccepted = true; + status.forEach((permission, status) { + if (status != PermissionStatus.granted) { + allAccepted = false; + } + }); + + if (allAccepted) { + // Show options for gallery or camera + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Wrap( + children: [ + ListTile( + leading: const Icon(Icons.photo), + title: const Text('Pick from Gallery'), + onTap: () async { + Navigator.of(context).pop(); + await _pickImage(ImageSource.gallery); + }, + ), + ListTile( + leading: const Icon(Icons.camera), + title: const Text('Capture with Camera'), + onTap: () async { + Navigator.of(context).pop(); + await _pickImage(ImageSource.camera); + }, + ), + ], + ); + }, + ); + } else { + print('Storage or camera permission denied'); + } + } + + Future _pickImage(ImageSource source) async { + XFile? pickedImage = await ImagePicker().pickImage( + source: source, + ); + + if (pickedImage != null) { + _imageFile = File(pickedImage.path); + _imageController.text = _imageFile!.path; + setState(() {}); + } + } + + Widget _buildSubmitButton() { + double screenWidth = MediaQuery.of(context).size.width; + double screenHeight = MediaQuery.of(context).size.height; + + return Container( + width: screenWidth * 0.8667, + height: screenHeight * 0.05625, + margin: const EdgeInsets.only(top: 20), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(50), + ), + child: InkWell( + onTap: () { + submitFood(); + }, + child: const Center( + child: Text( + 'Submit', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + letterSpacing: 0.36, + ), + ), + ), + ), + ); + } + + void submitFood() async { + // Check if all required fields are filled + if (_imageFile == null || _selectedFoodType.isEmpty) { + // Show an error message to the user + showErrorSnackbar(context, 'Please fill all required fields'); + return; + } + + try { + showLoadingDialog(context); + + String userId = FirebaseAuth.instance.currentUser!.uid; + UserProfile userProfile = UserProfile(); + String fullName = userProfile.getFullName(); + String venue = _venueController.text; + String address = _addressController.text; + String community = _communityController.text; + String date = _dateController.text; + String time = _timeController.text; + String toDate = _toDateController.text; + String toTime = _toTimeController.text; + + // Upload the image to Firebase Storage + String imageUrl = await uploadImageToFirebaseStorage(_imageFile, venue); + + // Create a map with food details, including a timestamp + Map foodData = { + 'userId': userId, + 'fullName': fullName, + 'imageUrl': imageUrl, + 'venue': venue, + 'address': address, + 'community': community, + 'foodType': _selectedFoodType, + 'date': date, + 'time': time, + 'toDate': toDate, + 'toTime': toTime, + 'timestamp': FieldValue.serverTimestamp(), + }; + // Save food data under the user's document in the 'sharedFood' collection + await FirebaseFirestore.instance + .collection('food') + .doc('sharedfood') + .collection("foodData") + .add(foodData); + + Navigator.of(context).pop(); + showSuccessSnackbar(context, 'Food submitted successfully!'); + Navigator.push(context, + MaterialPageRoute(builder: (context) => const ThankYouScreen())); + } catch (e) { + print('Error submitting food: $e'); + showErrorSnackbar(context, 'Error submitting food'); + } finally { + _venueController.clear(); + _addressController.clear(); + _communityController.clear(); + _imageController.clear(); + _dateController.clear(); + _timeController.clear(); + _toDateController.clear(); + _toTimeController.clear(); + _imageFile = null; + + setState(() { + _selectedFoodType = ''; + }); + } + } + + Widget _buildDropdownInput() { + return Container( + width: double.infinity, + height: 46, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.30, + color: Colors.black.withOpacity(0.6000000238418579), + ), + borderRadius: BorderRadius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: DropdownButton( + items: const [ + DropdownMenuItem( + value: 'veg', + child: Text('Veg'), + ), + DropdownMenuItem( + value: 'nonveg', + child: Text('Non-Veg'), + ), + DropdownMenuItem( + value: 'both', + child: Text('Both'), + ), + ], + onChanged: (value) { + setState(() { + _selectedFoodType = value!; + }); + }, + value: _selectedFoodType.isNotEmpty ? _selectedFoodType : null, + hint: const Text('Food Type'), + style: const TextStyle(color: Colors.black), + isExpanded: true, + ), + ), + ); + } + + Future uploadImageToFirebaseStorage( + File? imageFile, String venue) async { + if (imageFile == null) { + throw Exception('Image file is null'); + } + + try { + String fileName = 'food/shared_food/$venue.jpg'; + + firebase_storage.Reference storageReference = + firebase_storage.FirebaseStorage.instance.ref().child(fileName); + + await storageReference.putFile(imageFile); + + String downloadURL = await storageReference.getDownloadURL(); + + return downloadURL; + } catch (e) { + print('Error uploading image to Firebase Storage: $e'); + throw Exception('Error uploading image to Firebase Storage'); + } + } +} diff --git a/lib/screens/donate/thank_you.dart b/lib/screens/donate/thank_you.dart new file mode 100644 index 0000000..6bc6bad --- /dev/null +++ b/lib/screens/donate/thank_you.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/home/home.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; + +class ThankYouScreen extends StatelessWidget { + const ThankYouScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Timer(const Duration(seconds: 5), () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const HomeScreen(), + ), + ); + }); + + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/thankyou.jpg', // Add your thank you image asset path + height: 150, + width: 150, + // You can adjust the size based on your image dimensions + ), + const SizedBox(height: 20), + const Text( + 'Thank You!', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + const Text( + 'Your contribution is greatly appreciated.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } +} diff --git a/lib/screens/fooddetails/food_details.dart b/lib/screens/fooddetails/food_details.dart new file mode 100644 index 0000000..35f1136 --- /dev/null +++ b/lib/screens/fooddetails/food_details.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:intl/intl.dart'; + +class FoodDetailsScreen extends StatelessWidget { + final Map data; + + const FoodDetailsScreen({required this.data, Key? key}) : super(key: key); + +// Function to launch Google Maps with Directions + Future _launchMaps(String location) async { + final Uri uri = Uri.parse( + 'https://www.google.com/maps/dir/?api=1&destination=$location', + ); + await launchUrl(uri); + } + + String _formatDate(String date) { + final inputFormat = DateFormat('yyyy-MM-dd'); + final outputFormat = DateFormat('dd-MM-yyyy'); + final formattedDate = outputFormat.format(inputFormat.parse(date)); + return formattedDate; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ), + Text( + 'Food Details: ${data['venue'] ?? ''}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + width: 42, height: 42), + ], + ), + const SizedBox(height: 20), + Container( + width: 360, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: BorderRadius.circular(15), + ), + ), + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: BorderRadius.circular(15), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(0), + child: Image.network( + data['imageUrl'] ?? '', + height: 200, + width: double.infinity, + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + 'Venue: ${data['venue'] ?? ''}', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + 'Uploaded By: ${data['fullName'] ?? ''}', + style: const TextStyle(fontSize: 16), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + 'Location: ${data['address'] ?? ''}', + style: const TextStyle(fontSize: 16), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + 'Community: ${data['community'] ?? ''}', + style: const TextStyle(fontSize: 16), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + 'Food Type: ${data['foodType'] ?? ''}', + style: const TextStyle(fontSize: 16), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + border: TableBorder.all( + color: Colors.grey, + width: 1.0, + ), + children: [ + TableRow( + children: [ + TableCell( + child: _buildCell('From'), + ), + TableCell( + child: _buildCell('To'), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: _buildCell( + _formatDate('${data['date'] ?? ''}')), + ), + TableCell( + child: _buildCell( + _formatDate('${data['toDate'] ?? ''}')), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: _buildCell('${data['time'] ?? ''}'), + ), + TableCell( + child: _buildCell('${data['toTime'] ?? ''}'), + ), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: ElevatedButton( + onPressed: () => + _launchMaps(data['venue'] + data['address'] ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text('Get Directions'), + ), + ), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } + + Widget _buildCell(String text) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart new file mode 100644 index 0000000..61037ee --- /dev/null +++ b/lib/screens/home/home.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/home/home_page.dart'; +import 'package:spoonshare/models/users/user.dart'; +import 'package:spoonshare/screens/onboarding.dart'; +import 'package:spoonshare/widgets/random_quotes.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + UserProfile userProfile = UserProfile(); + + return FutureBuilder( + future: userProfile.loadUserProfile(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (!userProfile.isAuthenticated()) { + Future.delayed(Duration.zero, () { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const Onboarding()), + ); + }); + } + + String name = userProfile.getFullName(); + String role = userProfile.getRole(); + + return userProfile.isAuthenticated() + ? Scaffold( + body: Center( + child: HomePage(name: name, role: role), + ), + ) + : Container(); + } else { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/spoonshare_launcher.png', + width: 150, height: 150), + const SizedBox(height: 20), + Text( + 'Food Sharing Quote: ${RandomQuotes.getRandomFoodSharingQuote()}', + style: const TextStyle(fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + }, + ); + } +} diff --git a/lib/screens/home/home_page.dart b/lib/screens/home/home_page.dart new file mode 100644 index 0000000..9448579 --- /dev/null +++ b/lib/screens/home/home_page.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/donate/donate_page.dart'; +import 'package:spoonshare/screens/volunteer/volunteer_screen.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; +import 'package:spoonshare/widgets/nearby_food_cards.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key, required this.name, required this.role}) + : super(key: key); + final String name; + final String role; + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + int currentQuoteIndex = 0; + List quotes = [ + 'Share a meal, spread joy, and make a difference today.', + 'Nourishing hearts with kindness, one shared plate at a time.', + 'In the banquet of life, everyone deserves a seat and feast.', + ]; + + @override + void initState() { + super.initState(); + + _startChangingQuotes(); + } + + void _startChangingQuotes() { + Future.delayed(const Duration(seconds: 5), () { + if (mounted) { + setState(() { + currentQuoteIndex = (currentQuoteIndex + 1) % quotes.length; + }); + _startChangingQuotes(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hi ${widget.name}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.role, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: IconButton( + icon: const Icon(Icons.search), + onPressed: () {}, + ), + ), + const SizedBox(width: 8), + Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: IconButton( + icon: const Icon(Icons.notifications), + onPressed: () {}, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + Container( + width: 360, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: + BorderRadius.circular(15), // Added border radius + ), + ), + ), + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 208, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + const TextSpan( + text: '“', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + TextSpan( + text: quotes[currentQuoteIndex], + style: TextStyle( + color: Colors.black.withOpacity(0.8), + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + const TextSpan( + text: '”', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + height: 0, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 100, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: ShapeDecoration( + color: const Color(0xFF009E48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DonatePage(), + ), + ); + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Donate Food', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 6), + Text( + 'OR', + style: TextStyle( + color: Colors.black.withOpacity(0.4), + fontSize: 12, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: ShapeDecoration( + color: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const VolunteerScreen(), + ), + ); + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Become Volunteer', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Container( + margin: const EdgeInsets.only(top: 30), + alignment: Alignment.centerLeft, + child: const Text( + 'Near By Available Foods', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + NearbyFoodCard(), + const SizedBox(height: 5), + ], + ), + ], + ), + ), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } +} diff --git a/lib/screens/jobs/job_page.dart b/lib/screens/jobs/job_page.dart new file mode 100644 index 0000000..7822e2d --- /dev/null +++ b/lib/screens/jobs/job_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; + +class JobPage extends StatelessWidget { + const JobPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Text("Job Page"), + ), + bottomNavigationBar: BottomNavBar(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/onboarding.dart b/lib/screens/onboarding.dart new file mode 100644 index 0000000..ea66e35 --- /dev/null +++ b/lib/screens/onboarding.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/auth/signup.dart'; + +class Onboarding extends StatelessWidget { + const Onboarding({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final double screenHeight = MediaQuery.of(context).size.height; + + return Scaffold( + body: Center( + child: Container( + width: screenWidth, + height: screenHeight, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(color: Colors.white), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: screenWidth * 0.4667, + height: screenHeight * 0.04, + margin: const EdgeInsets.only(top: 12), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.86), + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Text( + 'MEALS OF GRACE BY', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + ), + ), + const Text( + 'SpoonShare', + style: TextStyle( + color: Colors.black, + fontSize: 42, + fontFamily: 'Roboto', + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + const Text( + 'अब भूखे नहीं रहेंगे.', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Asar', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + const SizedBox(height: 20), + const Center( + child: SizedBox( + width: 280, + child: Text.rich( + TextSpan( + text: 'Don’t throw,\nsend it to us.', + style: TextStyle( + color: Colors.black, + fontSize: 32, + fontStyle: FontStyle.italic, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + height: 0, + ), + ), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: 32), + Container( + width: screenWidth * 0.875, + height: screenHeight * 0.39, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/images/onboarding.png"), + fit: BoxFit.fill, + ), + ), + ), + SizedBox( + width: 280, + child: Text( + 'Don’t throw your food, send it to us we will give it to some in need and don’t have enough money to buy themself.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black.withOpacity(0.7799999713897705), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + height: 0, + ), + ), + ), + const SizedBox(height: 32), + Container( + width: screenWidth * 0.8667, + height: screenHeight * 0.05625, + margin: const EdgeInsets.only(top: 20), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(50), + ), + child: InkWell( + onTap: () { + // Navigate to another screen + Navigator.push( + context, + MaterialPageRoute(builder: (context) => SignUpScreen()), + ); + }, + child: const Center( + child: Text( + 'Get Started', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + letterSpacing: 0.36, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/profile/user_profile.dart b/lib/screens/profile/user_profile.dart new file mode 100644 index 0000000..cc74292 --- /dev/null +++ b/lib/screens/profile/user_profile.dart @@ -0,0 +1,637 @@ +// ignore_for_file: use_build_context_synchronously +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spoonshare/models/users/user.dart'; +import 'package:spoonshare/screens/volunteer/volunteer_screen.dart'; +import 'package:spoonshare/services/auth.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:spoonshare/widgets/loader.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'dart:io'; + +class UserProfileScreen extends StatefulWidget { + const UserProfileScreen({ + Key? key, + required this.name, + required this.role, + }) : super(key: key); + + final String name; + final String role; + + @override + _UserProfileScreenState createState() => _UserProfileScreenState(); +} + +class _UserProfileScreenState extends State { + bool isEditing = false; + AuthService authService = AuthService(); + + late TextEditingController nameController; + late TextEditingController emailController; + late TextEditingController contactController; + late TextEditingController orgController; + late TextEditingController roleController; + late TextEditingController profileImageController; + + @override + void initState() { + super.initState(); + nameController = TextEditingController(); + emailController = TextEditingController(); + contactController = TextEditingController(); + orgController = TextEditingController(); + roleController = TextEditingController(); + profileImageController = TextEditingController(); + loadUserProfileData(); + } + + Future loadUserProfileData() async { + UserProfile userProfile = UserProfile(); + await userProfile.loadUserProfile(); + + setState(() { + nameController.text = widget.name; + emailController.text = userProfile.getEmail(); + contactController.text = userProfile.getContactNumber(); + orgController.text = userProfile.getOrganisation(); + roleController.text = userProfile.getRole(); + profileImageController.text = userProfile.getProfileImageUrl(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hi ${widget.name}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.role, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: IconButton( + icon: const Icon(Icons.logout), + onPressed: () { + authService.signOut(context); + }, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Container( + width: 360, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + borderRadius: BorderRadius.circular(15), + ), + ), + ), + const SizedBox(height: 8), + Center( + child: SizedBox( + width: 312, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, // Center content + children: [ + const SizedBox(height: 20), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2, + color: Colors.black.withOpacity(0.1), + ), + ), + child: CircleAvatar( + radius: 80, + backgroundImage: profileImageController + .text.isNotEmpty + ? NetworkImage(profileImageController.text) + : const NetworkImage( + "https://www.shutterstock.com/image-vector/vector-flat-illustration-grayscale-avatar-600nw-2264922221.jpg", + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(6), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: IconButton( + icon: const Icon( + Icons.camera_alt, + color: Colors.black, + ), + onPressed: () async { + await pickFile(); + }), + ), + ), + ], + ), + const SizedBox( + height: 30, + ), + Container( + padding: const EdgeInsets.all(12), + width: 325, + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Colors.black.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.info_outline), + const SizedBox(width: 8), + const Text( + 'Personal Information', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + ), + const Spacer(), + isEditing + ? Row( + children: [ + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + setState(() { + isEditing = false; + }); + }, + ), + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + setState(() { + isEditing = false; + }); + }, + ), + ], + ) + : IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + setState(() { + isEditing = true; + }); + }, + ), + ], + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + height: 1, + color: Colors.black.withOpacity(0.2), + ), + const SizedBox(height: 16), + buildEditableField( + 'Name / नाम', + nameController, + TextInputType.text, + ), + const SizedBox(height: 16), + buildEditableField( + 'Email / ईमेल', + emailController, + TextInputType.emailAddress, + ), + const SizedBox(height: 16), + buildEditableField( + 'Contact / संपर्क', + contactController, + TextInputType.phone, + ), + const SizedBox(height: 16), + if (isEditing) + ElevatedButton( + onPressed: () async { + setState(() { + isEditing = false; + }); + await updateUserProfile(); + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + ), + child: const Text('Submit'), + ), + ], + ), + ), + const SizedBox( + height: 30, + ), + Container( + padding: const EdgeInsets.all(12), + width: 325, + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Colors.black.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.info_outline), + SizedBox(width: 8), + Text( + 'Organisation Information', + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + ), + /* const Spacer(), + isEditing + ? Row( + children: [ + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + setState(() { + isEditing = false; + }); + }, + ), + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + setState(() { + isEditing = false; + }); + }, + ), + ], + ) + : IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + setState(() { + isEditing = true; + }); + }, + ), + */ + ], + ), + const SizedBox(height: 10), + Container( + width: double.infinity, + height: 1, + color: Colors.black.withOpacity(0.2), + ), + const SizedBox(height: 16), + buildEditableField( + 'Organisation', + orgController, + TextInputType.text, + ), + const SizedBox(height: 16), + buildEditableField( + 'Role', + roleController, + TextInputType.text, + ), + const SizedBox(height: 16), + if (isEditing) + ElevatedButton( + onPressed: () async { + setState(() { + isEditing = false; + }); + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + ), + child: const Text('Submit'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + const VolunteerScreen(), + ), + ); + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + ), + child: const Text('Become A Volunteer'), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + bottomNavigationBar: const BottomNavBar(), + ); + } + + Widget buildEditableField( + String label, + TextEditingController controller, + TextInputType keyboardType, + ) { + return isEditing + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: Colors.black.withOpacity(0.4), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + ), + ), + TextFormField( + controller: controller, + keyboardType: keyboardType, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: Colors.black.withOpacity(0.4), + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + ), + ), + Text( + controller.text, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } + + Future pickFile() async { + final androidInfo = await DeviceInfoPlugin().androidInfo; + late final Map status; + if (androidInfo.version.sdkInt <= 32) { + status = await [ + Permission.storage, + ].request(); + } else { + status = await [Permission.photos, Permission.notification].request(); + } + + var allAccepted = true; + status.forEach((permission, status) { + if (status != PermissionStatus.granted) { + allAccepted = false; + } + }); + + if (allAccepted) { + XFile? pickedImage = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage != null) { + File imageFile = File(pickedImage.path); + + // Assuming you have a function to get the current user ID + String userId = FirebaseAuth.instance.currentUser!.uid; + + // Upload the image to Firebase Storage + String imageUrl = await uploadImageToFirebaseStorage(imageFile, userId); + + // Update the profile image URL + updateProfileImage(imageUrl); + + showSuccessSnackbar(context, "Profile Image Upload successfully!"); + } + } else { + showErrorSnackbar(context, 'Storage permission denied'); + print('Storage permission denied'); + } + } + + Future uploadImageToFirebaseStorage( + File imageFile, String userId) async { + try { + showLoadingDialog(context); + + String fileName = 'profile_$userId.jpg'; + Reference storageReference = + FirebaseStorage.instance.ref().child('users/profileImages/$fileName'); + + await storageReference.putFile(imageFile); + + String imageUrl = await storageReference.getDownloadURL(); + return imageUrl; + } catch (e) { + print('Error uploading image to Firebase Storage: $e'); + return ''; + } finally { + Navigator.pop(context); + } + } + + Future updateProfileImage(String imageUrl) async { + UserProfile userProfile = UserProfile(); + + try { + showLoadingDialog(context); + + setState(() { + userProfile.profileImageUrl = imageUrl; + profileImageController.text = imageUrl; + }); + + // Update in SharedPreferences + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('profileImageUrl', imageUrl); + + // Assuming you have a Firestore collection named 'users' + CollectionReference users = + FirebaseFirestore.instance.collection('users'); + + String userId = FirebaseAuth.instance.currentUser!.uid; + + // Perform the update + await users.doc(userId).update({ + 'profileImageUrl': imageUrl, + }); + + showSuccessSnackbar(context, "Profile image updated successfully!"); + + print("Profile image updated successfully!"); + } catch (e) { + // Handle the exception + showErrorSnackbar(context, "Error updating profile image: $e"); + print("Error updating profile image: $e"); + } finally { + Navigator.pop(context); + } + } + + Future updateUserProfile() async { + UserProfile userProfile = UserProfile(); + showLoadingDialog(context); // Show loader + try { + // Update locally + setState(() { + userProfile.fullName = nameController.text; + userProfile.email = emailController.text; + userProfile.contactNumber = contactController.text; + }); + + // Update in SharedPreferences + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('fullName', nameController.text); + prefs.setString('email', emailController.text); + prefs.setString('contactNumber', contactController.text); + + // Assuming you have a Firestore collection named 'users' + CollectionReference users = + FirebaseFirestore.instance.collection('users'); + + String userId = FirebaseAuth.instance.currentUser!.uid; + + // Perform the update + await users.doc(userId).update({ + 'fullName': nameController.text, + 'email': emailController.text, + 'contactNumber': contactController.text, + }); + + print("Updated successfully!"); + } catch (e) { + print("Error updating user profile: $e"); + } finally { + Navigator.pop(context); // Dismiss loader + } + } + +/* Future updateOrganisationAndRole() async { + UserProfile userProfile = UserProfile(); + + setState(() { + userProfile.organisation = orgController.text; + userProfile.role = roleController.text; + }); + + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('organisation', orgController.text); + prefs.setString('role', roleController.text); + + CollectionReference users = FirebaseFirestore.instance.collection('users'); + + String userId = FirebaseAuth.instance.currentUser!.uid; + + await users.doc(userId).update({ + 'organisation': orgController.text, + 'role': roleController.text, + }); + + print("Organisation and role updated successfully!"); + }*/ +} diff --git a/lib/screens/volunteer/volunteer_screen.dart b/lib/screens/volunteer/volunteer_screen.dart new file mode 100644 index 0000000..6aad87c --- /dev/null +++ b/lib/screens/volunteer/volunteer_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/widgets/bottom_navbar.dart'; + +class VolunteerScreen extends StatelessWidget { + const VolunteerScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Text('Volunteer Screen'), + ), + bottomNavigationBar: BottomNavBar(), + ); + } +} \ No newline at end of file diff --git a/lib/services/auth.dart b/lib/services/auth.dart new file mode 100644 index 0000000..44ee0a2 --- /dev/null +++ b/lib/services/auth.dart @@ -0,0 +1,40 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spoonshare/screens/auth/signin.dart'; +import 'package:spoonshare/widgets/snackbar.dart'; +import 'package:flutter/material.dart'; + +class AuthService { + final FirebaseAuth _auth = FirebaseAuth.instance; + + // Check if the user is authenticated + Future isAuthenticated() async { + return _auth.currentUser != null; + } + + // Sign out the user + Future signOut(BuildContext context) async { + try { + await _clearLocalUserData(); + await _auth.signOut(); + + showSuccessSnackbar(context, "Sign Out Successful"); + + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SignInScreen()), + ); + } catch (e) { + print('Error during logout: $e'); + showErrorSnackbar(context, "Sign Out Failed"); + } + } + + // Clear local user data from shared preferences + Future _clearLocalUserData() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.clear(); + } +} diff --git a/lib/services/forgot_password.dart b/lib/services/forgot_password.dart new file mode 100644 index 0000000..bf3f0c0 --- /dev/null +++ b/lib/services/forgot_password.dart @@ -0,0 +1,13 @@ +import 'package:firebase_auth/firebase_auth.dart'; + +class ForgotPasswordService { + final FirebaseAuth _auth = FirebaseAuth.instance; + + Future resetPassword(String email) async { + try { + await _auth.sendPasswordResetEmail(email: email); + } catch (e) { + throw e; + } + } +} diff --git a/lib/splash_screen.dart b/lib/splash_screen.dart new file mode 100644 index 0000000..64c625b --- /dev/null +++ b/lib/splash_screen.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/home/home.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({Key? key}) : super(key: key); + + @override + _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + Timer(const Duration(seconds: 3), () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => + const HomeScreen(), + ) + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/spoonshare_launcher.png', width: 150, height: 150), + const SizedBox(height: 20), + const CircularProgressIndicator( + color: Colors.black45, + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/bottom_navbar.dart b/lib/widgets/bottom_navbar.dart new file mode 100644 index 0000000..7ec7b95 --- /dev/null +++ b/lib/widgets/bottom_navbar.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:spoonshare/models/users/user.dart'; +import 'package:spoonshare/screens/chat/chat_page.dart'; +import 'package:spoonshare/screens/donate/donate_page.dart'; +import 'package:spoonshare/screens/home/home_page.dart'; +import 'package:spoonshare/screens/jobs/job_page.dart'; +import 'package:spoonshare/screens/profile/user_profile.dart'; + +class BottomNavBar extends StatelessWidget { + const BottomNavBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final UserProfile userProfile = UserProfile(); + final String name = userProfile.getFullName(); + final String role = userProfile.getRole(); + + return Container( + width: double.infinity, + height: 67, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF0F2F3).withOpacity(0.5), + border: Border.all(color: const Color(0xFFF0F2F3)), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildNavItem(context, Icons.home, 'Home', Colors.black54, + Colors.black54, HomePage(name: name, role: role)), + _buildNavItem(context, Icons.chat, 'Chat', Colors.black54, + Colors.black54, const ChatPage()), + _buildNavItem(context, Icons.add_circle, 'Donate Food', + Colors.black54, Colors.black54, const DonatePage()), + _buildNavItem(context, Icons.work, 'Jobs', Colors.black54, + Colors.black54, const JobPage()), + _buildNavItem( + context, + Icons.person, + 'Profile', + Colors.black54, + Colors.black54, + UserProfileScreen( + name: name, + role: role, + )), + ], + ), + ); + } + + Widget _buildNavItem(BuildContext context, IconData icon, String label, + Color iconColor, Color textColor, Widget destination) { + bool isSelected = ModalRoute.of(context)?.settings.name == label; + + return Expanded( + child: InkWell( + onTap: () { + if (isSelected) { + Navigator.popUntil(context, (route) => route.isFirst); + } else { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + destination, + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + const begin = Offset(-1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutQuart; + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + return SlideTransition( + position: offsetAnimation, child: child); + }, + ), + ); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + icon, + size: 26, + color: Colors.black, + ), + const SizedBox(height: 2), + Text( + label, + style: const TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/custom_text_field.dart b/lib/widgets/custom_text_field.dart new file mode 100644 index 0000000..40f44d0 --- /dev/null +++ b/lib/widgets/custom_text_field.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class CustomTextField extends StatelessWidget { + final String label; + final TextEditingController controller; + + const CustomTextField({ + Key? key, + required this.label, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.black, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.black, + ), + ), + labelStyle: const TextStyle(color: Colors.black), + ), + ); + } +} diff --git a/lib/widgets/loader.dart b/lib/widgets/loader.dart new file mode 100644 index 0000000..8c4681f --- /dev/null +++ b/lib/widgets/loader.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +void showLoadingDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, // Prevent dismissing by tapping outside + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); +} diff --git a/lib/widgets/nearby_food_cards.dart b/lib/widgets/nearby_food_cards.dart new file mode 100644 index 0000000..d6940ae --- /dev/null +++ b/lib/widgets/nearby_food_cards.dart @@ -0,0 +1,117 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:spoonshare/screens/fooddetails/food_details.dart'; + +class NearbyFoodCard extends StatelessWidget { + const NearbyFoodCard({super.key}); + + void _navigateToFoodDetails(BuildContext context, Map data) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FoodDetailsScreen(data: data), + ), + ); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder>>( + stream: FirebaseFirestore.instance + .collection('food') + .doc('sharedfood') + .collection('foodData') + .orderBy('timestamp', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + + List>> foodDocs = + snapshot.data?.docs ?? []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + ...(foodDocs.isEmpty + ? [const Text('No data available.')] + : foodDocs.map((doc) { + Map data = doc.data(); + return GestureDetector( + onTap: () => _navigateToFoodDetails(context, data), + child: Card( + margin: const EdgeInsets.all(8), + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(10)), + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Image.network( + data['imageUrl'] ?? '', + height: 200, + fit: BoxFit.cover, + ), + ), + ), + Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data['venue'] ?? '', + style: const TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Uploaded By: ${data['fullName'] ?? ''}', + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + 'Location: ${data['address'] ?? ''}', + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ), + ); + })), + const SizedBox(height: 5), + ], + ); + }, + ); + } +} diff --git a/lib/widgets/random_quotes.dart b/lib/widgets/random_quotes.dart new file mode 100644 index 0000000..0c01f82 --- /dev/null +++ b/lib/widgets/random_quotes.dart @@ -0,0 +1,15 @@ +import 'dart:math'; + +class RandomQuotes { + static final List foodSharingQuotes = [ + 'Sharing food is sharing love.', + 'A shared meal is a shared joy.', + 'Food is better when shared with friends.', + 'The more you share, the more you have.', + ]; + + static String getRandomFoodSharingQuote() { + final Random random = Random(); + return foodSharingQuotes[random.nextInt(foodSharingQuotes.length)]; + } +} diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart new file mode 100644 index 0000000..be12bc6 --- /dev/null +++ b/lib/widgets/snackbar.dart @@ -0,0 +1,59 @@ +import "package:flutter/material.dart"; + +void showSuccessSnackbar(BuildContext context, String message) { + final snackBar = SnackBar( + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ), + backgroundColor: Colors.green, + elevation: 6.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + action: SnackBarAction( + label: 'OK', + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + duration: const Duration(seconds: 2), // Set the duration here + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); +} + +void showErrorSnackbar(BuildContext context, String message) { + final snackBar = SnackBar( + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ), + backgroundColor: Colors.red, + elevation: 6.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + action: SnackBarAction( + label: 'OK', + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + duration: const Duration(seconds: 2), // Set the duration here + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..c4f7053 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,890 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + url: "https://pub.dev" + source: hosted + version: "1.3.16" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "8bfbb5a2edbc6052452326d60de0113fea2bcbf081d34a3f8e45c8b38307b31c" + url: "https://pub.dev" + source: hosted + version: "4.14.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.dev" + source: hosted + version: "0.3.3+8" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "279b2773ff61afd9763202cb5582e2b995ee57419d826b9af6517302a59b672f" + url: "https://pub.dev" + source: hosted + version: "4.16.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "3c9cfaccb7549492edf5b0c67c6dd1c6727c7830891aa6727f2fb225f0226626" + url: "https://pub.dev" + source: hosted + version: "7.0.9" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: c7b1379ccef7abf4b6816eede67a868c44142198e42350f51c01d8fc03f95a7d + url: "https://pub.dev" + source: hosted + version: "5.8.13" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + url: "https://pub.dev" + source: hosted + version: "2.24.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "75e6cb6bed65138b5bbd86bfd7cf9bc9a175fb0c31aacc400e9203df117ffbe6" + url: "https://pub.dev" + source: hosted + version: "11.6.0" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "545a3a8edf337850403bb0fa03c8074a53deb87c0107d19755c77a82ce07919e" + url: "https://pub.dev" + source: hosted + version: "5.1.3" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: ee6870ff79aa304b8996ba18a4aefe1e8b3fc31fd385eab6574180267aa8d393 + url: "https://pub.dev" + source: hosted + version: "3.6.17" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" + 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" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + url: "https://pub.dev" + source: hosted + version: "0.3.0+2" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: bfd42c81c30c6faba16e0f62968d5505a87504aaa672b3155ee931461abb0a49 + url: "https://pub.dev" + source: hosted + version: "6.1.21" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: f3336d9e44d4d28063ac90271f6db5caf99f0480cb07281330e7a432edb95226 + url: "https://pub.dev" + source: hosted + version: "5.7.3" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: a278ea2d01013faf341cbb093da880d0f2a552bbd1cb6ee90b5bebac9ba69d77 + url: "https://pub.dev" + source: hosted + version: "0.12.3+2" + http: + dependency: transitive + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + url: "https://pub.dev" + source: hosted + version: "0.8.9+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + url: "https://pub.dev" + source: hosted + version: "0.8.9+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + url: "https://pub.dev" + source: hosted + version: "2.9.3" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + url: "https://pub.dev" + source: hosted + version: "11.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + url: "https://pub.dev" + source: hosted + version: "9.3.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + persistent_bottom_nav_bar: + dependency: "direct main" + description: + name: persistent_bottom_nav_bar + sha256: e2e031e3366fde01511e372a9a19d720a9b252bb456e5c8d77a30d7a4a2ddfb0 + url: "https://pub.dev" + source: hosted + version: "5.0.2" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 + url: "https://pub.dev" + source: hosted + version: "6.2.3" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.4 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..e342df7 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,82 @@ +name: spoonshare +description: "Meals of Grace." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.4 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + firebase_core: ^2.24.2 + firebase_auth: ^4.16.0 + cloud_firestore: ^4.14.0 + shared_preferences: ^2.2.2 + flutter_dotenv: ^5.1.0 + google_sign_in: ^6.2.1 + persistent_bottom_nav_bar: ^5.0.2 + image_picker: ^1.0.7 + firebase_storage: ^11.6.0 + permission_handler: ^11.2.0 + flutter_launcher_icons: ^0.13.1 + device_info_plus: ^9.1.1 + url_launcher: ^6.2.3 + intl: ^0.19.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + + uses-material-design: true + + assets: + - assets/images/onboarding.png + - assets/images/google.png + - assets/images/mail.png + - assets/images/spoonshare_launcher.png + - assets/images/thankyou.jpg + + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/images/spoonshare_launcher.png" + min_sdk_android: 21 + web: + generate: true + image_path: "assets/images/spoonshare_launcher.png" + background_color: "#hexcode" + theme_color: "#hexcode" + + + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..c8b2331 Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..d443fae Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..8b9f694 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..d443fae Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..8b9f694 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f04b7a0 --- /dev/null +++ b/web/index.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + SpoonShare + + + + + + + + + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..07d4859 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "spoonsharemeals", + "short_name": "spoonsharemeals", + "start_url": ".", + "display": "standalone", + "background_color": "#hexcode", + "theme_color": "#hexcode", + "description": "SpoonShare - Meal of grace", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file