├── .eslintignore ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile └── Podfile.lock ├── analysis_options.yaml ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── amplify_trips_planner │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── images └── amplify.png ├── amplify ├── backend │ ├── api │ │ └── amplifytripsplanner │ │ │ ├── transform.conf.json │ │ │ ├── resolvers │ │ │ └── README.md │ │ │ ├── parameters.json │ │ │ ├── cli-inputs.json │ │ │ ├── schema.graphql │ │ │ └── stacks │ │ │ └── CustomResources.json │ ├── function │ │ └── amplifytripsplanner4600aecaPostConfirmation │ │ │ ├── custom-policies.json │ │ │ ├── parameters.json │ │ │ ├── src │ │ │ ├── event.json │ │ │ ├── package.json │ │ │ ├── index.js │ │ │ └── custom.js │ │ │ ├── amplify.state │ │ │ └── function-parameters.json │ ├── tags.json │ ├── storage │ │ └── s37d9e620f │ │ │ └── cli-inputs.json │ ├── types │ │ └── amplify-dependent-resources-ref.d.ts │ ├── auth │ │ └── amplifytripsplanner4600aeca │ │ │ └── cli-inputs.json │ └── backend-config.json ├── .config │ └── project-config.json ├── README.md ├── hooks │ └── README.md └── cli.json ├── lib ├── common │ ├── navigation │ │ └── router │ │ │ ├── routes.dart │ │ │ └── router.dart │ ├── utils │ │ ├── date_time_formatter.dart │ │ └── colors.dart │ ├── services │ │ ├── auth_service.dart │ │ └── storage_service.dart │ └── ui │ │ ├── bottomsheet_text_form_field.dart │ │ ├── upload_progress_dialog.dart │ │ └── the_navigation_drawer.dart ├── features │ ├── trip │ │ ├── controller │ │ │ ├── past_trips_list.dart │ │ │ ├── trips_list.g.dart │ │ │ ├── past_trips_list.g.dart │ │ │ ├── trips_list.dart │ │ │ ├── trip_controller.dart │ │ │ └── trip_controller.g.dart │ │ ├── ui │ │ │ ├── trip_page │ │ │ │ ├── delete_trip_dialog.dart │ │ │ │ ├── trip_page_floating_button.dart │ │ │ │ ├── trip_page.dart │ │ │ │ ├── trip_details.dart │ │ │ │ └── selected_trip_card.dart │ │ │ ├── past_trips │ │ │ │ └── past_trips_list.dart │ │ │ ├── trips_gridview │ │ │ │ ├── trip_gridview_item.dart │ │ │ │ ├── trips_list_gridview.dart │ │ │ │ └── trip_gridview_item_card.dart │ │ │ ├── past_trip_page │ │ │ │ ├── past_trip_page.dart │ │ │ │ ├── past_trip_details.dart │ │ │ │ └── selected_past_trip_card.dart │ │ │ ├── trips_list │ │ │ │ ├── trips_list_page.dart │ │ │ │ └── add_trip_bottomsheet.dart │ │ │ └── edit_trip_page │ │ │ │ └── edit_trip_page.dart │ │ ├── data │ │ │ └── trips_repository.dart │ │ └── service │ │ │ └── trips_api_service.dart │ ├── activity │ │ ├── ui │ │ │ ├── activity_page │ │ │ │ ├── delete_activity_dialog.dart │ │ │ │ ├── activity_page_appbar_icon.dart │ │ │ │ ├── activity_page.dart │ │ │ │ └── activity_listview.dart │ │ │ ├── activities_list │ │ │ │ ├── activities_list.dart │ │ │ │ └── activities_timeline.dart │ │ │ ├── activity_category_icon.dart │ │ │ ├── add_activity │ │ │ │ ├── add_activity_page.dart │ │ │ │ └── add_activity_form.dart │ │ │ └── edit_activity │ │ │ │ └── edit_activity_page.dart │ │ ├── data │ │ │ └── activities_repository.dart │ │ ├── controller │ │ │ ├── activity_controller.dart │ │ │ ├── activities_list.dart │ │ │ ├── activities_list.g.dart │ │ │ └── activity_controller.g.dart │ │ └── service │ │ │ └── activities_api_service.dart │ └── profile │ │ ├── data │ │ └── profile_repository.dart │ │ ├── controller │ │ ├── profile_controller.dart │ │ └── profile_controller.g.dart │ │ ├── ui │ │ └── profile_page │ │ │ ├── profile_page.dart │ │ │ ├── edit_profile_bottomsheet.dart │ │ │ └── profile_listview.dart │ │ └── service │ │ └── profile_api_service.dart ├── trips_planner_app.dart ├── main.dart └── models │ ├── ActivityCategory.dart │ └── ModelProvider.dart ├── .vscode └── settings.json ├── README.md ├── pubspec.yaml ├── .metadata ├── test └── widget_test.dart └── .gitignore /.eslintignore: -------------------------------------------------------------------------------- 1 | amplify-codegen-temp/models -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:amplify_lints/app.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - lib/models/** 6 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /images/amplify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/images/amplify.png -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/custom-policies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Action": [], 4 | "Resource": [] 5 | } 6 | ] -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": "custom", 3 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation" 4 | } -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "userPoolId": "testID", 4 | "userName": "testUser" 5 | }, 6 | "response": {} 7 | } 8 | -------------------------------------------------------------------------------- /amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/common/navigation/router/routes.dart: -------------------------------------------------------------------------------- 1 | enum AppRoute { 2 | home, 3 | trip, 4 | editTrip, 5 | pastTrips, 6 | pastTrip, 7 | activity, 8 | addActivity, 9 | editActivity, 10 | profile, 11 | } 12 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/offlineprogrammer/amplify_trips_planner_tutorial/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/common/utils/date_time_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateTimeFormatter on DateTime { 4 | String format(String format) { 5 | return DateFormat(format).format(this); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/amplify_trips_planner/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.amplify_trips_planner 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "amplifytripsplanner", 3 | "version": "3.1", 4 | "frontend": "flutter", 5 | "flutter": { 6 | "config": { 7 | "ResDir": "./lib/" 8 | } 9 | }, 10 | "providers": [ 11 | "awscloudformation" 12 | ] 13 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/resolvers/README.md: -------------------------------------------------------------------------------- 1 | Any resolvers that you add in this directory will override the ones automatically generated by Amplify CLI and will be directly copied to the cloud. 2 | For more information, visit [https://docs.amplify.aws/cli/graphql-transformer/resolvers](https://docs.amplify.aws/cli/graphql-transformer/resolvers) -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /amplify/backend/storage/s37d9e620f/cli-inputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "s37d9e620f", 3 | "policyUUID": "7d9e620f", 4 | "bucketName": "amptripsplanner03", 5 | "storageAccess": "authAndGuest", 6 | "guestAccess": [ 7 | "READ" 8 | ], 9 | "authAccess": [ 10 | "CREATE_AND_UPDATE", 11 | "READ", 12 | "DELETE" 13 | ], 14 | "triggerFunction": "NONE", 15 | "groupAccess": {} 16 | } -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "amplifytripsplanner", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authamplifytripsplanner4600aeca", 8 | "Outputs.UserPoolId" 9 | ] 10 | }, 11 | "AuthModeLastUpdated": "2023-03-19T00:40:07.402Z" 12 | } -------------------------------------------------------------------------------- /amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/cli-inputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "serviceConfiguration": { 4 | "apiName": "amplifytripsplanner", 5 | "serviceName": "AppSync", 6 | "defaultAuthType": { 7 | "mode": "AMAZON_COGNITO_USER_POOLS", 8 | "cognitoUserPoolId": "authamplifytripsplanner4600aeca" 9 | }, 10 | "conflictResolution": {}, 11 | "additionalAuthTypes": [ 12 | { 13 | "mode": "AWS_IAM" 14 | } 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "amplify/.config": true, 4 | "amplify/**/*-parameters.json": true, 5 | "amplify/**/amplify.state": true, 6 | "amplify/**/transform.conf.json": true, 7 | "amplify/#current-cloud-backend": true, 8 | "amplify/backend/amplify-meta.json": true, 9 | "amplify/backend/awscloudformation": true 10 | }, 11 | "[dart]": { 12 | "editor.formatOnSave": true, 13 | "editor.formatOnType": true 14 | }, 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll": true, 17 | "source.organizeImports": true 18 | } 19 | } -------------------------------------------------------------------------------- /lib/common/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_flutter/amplify_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | final authServiceProvider = Provider((ref) { 6 | return AuthService(); 7 | }); 8 | 9 | class AuthService { 10 | AuthService(); 11 | 12 | Future signOut() async { 13 | try { 14 | await Amplify.Auth.signOut(); 15 | await Amplify.DataStore.clear(); 16 | } on Exception catch (e) { 17 | debugPrint(e.toString()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amplify_trips_planner 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amplifytripsplanner4600aecaPostConfirmation", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "axios": "latest", 9 | "@aws-crypto/sha256-js": "^2.0.1", 10 | "@aws-sdk/credential-provider-node": "^3.76.0", 11 | "@aws-sdk/protocol-http": "^3.58.0", 12 | "@aws-sdk/signature-v4": "^3.58.0", 13 | "node-fetch": "2" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.92" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/features/trip/controller/past_trips_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:amplify_trips_planner/features/trip/data/trips_repository.dart'; 4 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | part 'past_trips_list.g.dart'; 8 | 9 | @riverpod 10 | class PastTripsList extends _$PastTripsList { 11 | Future> _fetchTrips() async { 12 | final tripsRepository = ref.read(tripsRepositoryProvider); 13 | final trips = await tripsRepository.getPastTrips(); 14 | return trips; 15 | } 16 | 17 | @override 18 | FutureOr> build() async { 19 | return _fetchTrips(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trip_page/delete_trip_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DeleteTripDialog extends StatelessWidget { 4 | const DeleteTripDialog({ 5 | super.key, 6 | }); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return AlertDialog( 11 | title: const Text('Please Confirm'), 12 | content: const Text('Delete this trip?'), 13 | actions: [ 14 | TextButton( 15 | onPressed: () async { 16 | Navigator.of(context).pop(true); 17 | }, 18 | child: const Text('Yes'), 19 | ), 20 | TextButton( 21 | onPressed: () { 22 | Navigator.of(context).pop(false); 23 | }, 24 | child: const Text('No'), 25 | ) 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activity_page/delete_activity_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DeleteActivityDialog extends StatelessWidget { 4 | const DeleteActivityDialog({ 5 | super.key, 6 | }); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return AlertDialog( 11 | title: const Text('Please Confirm'), 12 | content: const Text('Delete this activity?'), 13 | actions: [ 14 | TextButton( 15 | onPressed: () async { 16 | Navigator.of(context).pop(true); 17 | }, 18 | child: const Text('Yes'), 19 | ), 20 | TextButton( 21 | onPressed: () { 22 | Navigator.of(context).pop(false); 23 | }, 24 | child: const Text('No'), 25 | ) 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/profile/data/profile_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/profile/service/profile_api_service.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | final profileRepositoryProvider = Provider((ref) { 6 | final profileAPIService = ref.read(profileAPIServiceProvider); 7 | return ProfileRepository(profileAPIService); 8 | }); 9 | 10 | class ProfileRepository { 11 | ProfileRepository(this.profileAPIService); 12 | final ProfileAPIService profileAPIService; 13 | 14 | Future getProfile() { 15 | return profileAPIService.getProfile(); 16 | } 17 | 18 | Future update(Profile updatedProfile) async { 19 | return profileAPIService.updateProfile(updatedProfile); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/trips_planner_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_authenticator/amplify_authenticator.dart'; 2 | import 'package:amplify_trips_planner/common/navigation/router/router.dart'; 3 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 4 | import 'package:flutter/material.dart'; 5 | 6 | class TripsPlannerApp extends StatelessWidget { 7 | const TripsPlannerApp({ 8 | super.key, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Authenticator( 14 | child: MaterialApp.router( 15 | routerConfig: router, 16 | builder: Authenticator.builder(), 17 | theme: ThemeData( 18 | colorScheme: 19 | ColorScheme.fromSwatch(primarySwatch: constants.primaryColor) 20 | .copyWith( 21 | background: const Color(0xff82CFEA), 22 | ), 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "trigger": true, 3 | "modules": [ 4 | "custom" 5 | ], 6 | "parentResource": "amplifytripsplanner4600aeca", 7 | "functionName": "amplifytripsplanner4600aecaPostConfirmation", 8 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation", 9 | "parentStack": "auth", 10 | "triggerEnvs": "[]", 11 | "triggerDir": "/snapshot/repo/build/node_modules/@aws-amplify/amplify-category-auth/provider-utils/awscloudformation/triggers/PostConfirmation", 12 | "triggerTemplate": "PostConfirmation.json.ejs", 13 | "triggerEventPath": "PostConfirmation.event.json", 14 | "roleName": "amplifytripsplanner4600aecaPostConfirmation", 15 | "skipEdit": true, 16 | "permissions": { 17 | "api": { 18 | "amplifytripsplanner": [ 19 | "Query", 20 | "Mutation", 21 | "Subscription" 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/common/utils/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const Map primarySwatch = { 4 | 50: Color.fromRGBO(255, 207, 68, .1), 5 | 100: Color.fromRGBO(255, 207, 68, .2), 6 | 200: Color.fromRGBO(255, 207, 68, .3), 7 | 300: Color.fromRGBO(255, 207, 68, .4), 8 | 400: Color.fromRGBO(255, 207, 68, .5), 9 | 500: Color.fromRGBO(255, 207, 68, .6), 10 | 600: Color.fromRGBO(255, 207, 68, .7), 11 | 700: Color.fromRGBO(255, 207, 68, .8), 12 | 800: Color.fromRGBO(255, 207, 68, .9), 13 | 900: Color.fromRGBO(255, 207, 68, 1), 14 | }; 15 | const MaterialColor primaryColor = MaterialColor(0xFFFFCF44, primarySwatch); 16 | const int primaryColorDark = 0xFFFD9725; 17 | const List greyoutMatrix = [ 18 | 0.2126, 19 | 0.7152, 20 | 0.0722, 21 | 0, 22 | 0, 23 | 0.2126, 24 | 0.7152, 25 | 0.0722, 26 | 0, 27 | 0, 28 | 0.2126, 29 | 0.7152, 30 | 0.0722, 31 | 0, 32 | 0, 33 | 0, 34 | 0, 35 | 0, 36 | 1, 37 | 0, 38 | ]; 39 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: amplify_trips_planner 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=3.0.2 <4.0.0" 7 | 8 | dependencies: 9 | amplify_api: ^1.0.0 10 | amplify_auth_cognito: ^1.0.0 11 | amplify_authenticator: ^1.0.0 12 | amplify_flutter: ^1.0.0 13 | amplify_storage_s3: ^1.0.0 14 | cached_network_image: ^3.2.3 15 | cupertino_icons: ^1.0.5 16 | file_picker: ^5.0.0 17 | flutter: 18 | sdk: flutter 19 | flutter_riverpod: ^2.1.3 20 | go_router: ^7.0.0 21 | image_picker: ^0.8.0 22 | intl: ^0.18.0 23 | path: ^1.8.3 24 | riverpod_annotation: ^2.0.1 25 | timelines: ^0.1.0 26 | url_launcher: ^6.1.5 27 | uuid: ^3.0.7 28 | 29 | dev_dependencies: 30 | amplify_lints: ^3.0.0 31 | build_runner: 32 | custom_lint: 33 | flutter_test: 34 | sdk: flutter 35 | riverpod_generator: ^2.1.3 36 | riverpod_lint: ^1.1.5 37 | 38 | flutter: 39 | uses-material-design: true 40 | assets: 41 | - images/amplify.png 42 | -------------------------------------------------------------------------------- /amplify/backend/types/amplify-dependent-resources-ref.d.ts: -------------------------------------------------------------------------------- 1 | export type AmplifyDependentResourcesAttributes = { 2 | "api": { 3 | "amplifytripsplanner": { 4 | "GraphQLAPIEndpointOutput": "string", 5 | "GraphQLAPIIdOutput": "string" 6 | } 7 | }, 8 | "auth": { 9 | "amplifytripsplanner4600aeca": { 10 | "AppClientID": "string", 11 | "AppClientIDWeb": "string", 12 | "IdentityPoolId": "string", 13 | "IdentityPoolName": "string", 14 | "UserPoolArn": "string", 15 | "UserPoolId": "string", 16 | "UserPoolName": "string" 17 | } 18 | }, 19 | "function": { 20 | "amplifytripsplanner4600aecaPostConfirmation": { 21 | "Arn": "string", 22 | "LambdaExecutionRole": "string", 23 | "LambdaExecutionRoleArn": "string", 24 | "Name": "string", 25 | "Region": "string" 26 | } 27 | }, 28 | "storage": { 29 | "s37d9e620f": { 30 | "BucketName": "string", 31 | "Region": "string" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /lib/features/profile/controller/profile_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/profile/data/profile_repository.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'profile_controller.g.dart'; 6 | 7 | @riverpod 8 | class ProfileController extends _$ProfileController { 9 | Future _fetchProfile() async { 10 | final profileRepository = ref.read(profileRepositoryProvider); 11 | return profileRepository.getProfile(); 12 | } 13 | 14 | @override 15 | FutureOr build() async { 16 | return _fetchProfile(); 17 | } 18 | 19 | Future updateProfile(Profile profile) async { 20 | state = const AsyncValue.loading(); 21 | state = await AsyncValue.guard(() async { 22 | final profileRepository = ref.read(profileRepositoryProvider); 23 | await profileRepository.update(profile); 24 | return _fetchProfile(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f72efea43c3013323d1b95cff571f3c1caa37583 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 17 | base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 18 | - platform: ios 19 | create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 20 | base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /lib/features/trip/controller/trips_list.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'trips_list.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$tripsListHash() => r'3e51ec4baddbb143787d53cbfba723bfb9a88aaa'; 10 | 11 | /// See also [TripsList]. 12 | @ProviderFor(TripsList) 13 | final tripsListProvider = 14 | AutoDisposeAsyncNotifierProvider>.internal( 15 | TripsList.new, 16 | name: r'tripsListProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$tripsListHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$TripsList = AutoDisposeAsyncNotifier>; 24 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 25 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/common/ui/bottomsheet_text_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BottomSheetTextFormField extends StatelessWidget { 4 | const BottomSheetTextFormField({ 5 | required this.labelText, 6 | required this.controller, 7 | required this.keyboardType, 8 | this.onTap, 9 | super.key, 10 | }); 11 | 12 | final String labelText; 13 | final TextEditingController controller; 14 | final TextInputType keyboardType; 15 | final void Function()? onTap; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return TextFormField( 20 | controller: controller, 21 | keyboardType: keyboardType, 22 | autofocus: true, 23 | autocorrect: false, 24 | textInputAction: TextInputAction.next, 25 | validator: (value) { 26 | if (value == null || value.isEmpty) { 27 | return 'Please enter a value'; 28 | } 29 | 30 | return null; 31 | }, 32 | decoration: InputDecoration( 33 | labelText: labelText, 34 | ), 35 | onTap: onTap, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/features/trip/controller/past_trips_list.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'past_trips_list.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$pastTripsListHash() => r'59549eda47044c05047c3093fbe2f5aacf35bfa9'; 10 | 11 | /// See also [PastTripsList]. 12 | @ProviderFor(PastTripsList) 13 | final pastTripsListProvider = 14 | AutoDisposeAsyncNotifierProvider>.internal( 15 | PastTripsList.new, 16 | name: r'pastTripsListProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$pastTripsListHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$PastTripsList = AutoDisposeAsyncNotifier>; 25 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 26 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_api/amplify_api.dart'; 2 | import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; 3 | import 'package:amplify_flutter/amplify_flutter.dart'; 4 | import 'package:amplify_storage_s3/amplify_storage_s3.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:amplify_trips_planner/trips_planner_app.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | 10 | import 'amplifyconfiguration.dart'; 11 | 12 | Future main() async { 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | try { 15 | await _configureAmplify(); 16 | } on AmplifyAlreadyConfiguredException { 17 | debugPrint('Amplify configuration failed.'); 18 | } 19 | 20 | runApp( 21 | const ProviderScope( 22 | child: TripsPlannerApp(), 23 | ), 24 | ); 25 | } 26 | 27 | Future _configureAmplify() async { 28 | await Amplify.addPlugins([ 29 | AmplifyAuthCognito(), 30 | AmplifyAPI(modelProvider: ModelProvider.instance), 31 | AmplifyStorageS3() 32 | ]); 33 | await Amplify.configure(amplifyconfig); 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/profile/ui/profile_page/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/features/profile/controller/profile_controller.dart'; 4 | import 'package:amplify_trips_planner/features/profile/ui/profile_page/profile_listview.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class ProfilePage extends ConsumerWidget { 9 | const ProfilePage({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final profileValue = ref.watch(profileControllerProvider); 16 | return Scaffold( 17 | appBar: AppBar( 18 | centerTitle: true, 19 | title: const Text( 20 | 'Amplify Trips Planner', 21 | ), 22 | backgroundColor: const Color(constants.primaryColorDark), 23 | ), 24 | drawer: const TheNavigationDrawer(), 25 | body: ProfileListView( 26 | profile: profileValue, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/features/profile/controller/profile_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'profile_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$profileControllerHash() => r'09d513dcea0ba43f01ade4141fd877b04e540df7'; 10 | 11 | /// See also [ProfileController]. 12 | @ProviderFor(ProfileController) 13 | final profileControllerProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | ProfileController.new, 16 | name: r'profileControllerProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$profileControllerHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$ProfileController = AutoDisposeAsyncNotifier; 25 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 26 | -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/schema.graphql: -------------------------------------------------------------------------------- 1 | type Trip @model @auth(rules: [{ allow: owner }]) { 2 | id: ID! 3 | tripName: String! 4 | destination: String! 5 | startDate: AWSDate! 6 | endDate: AWSDate! 7 | tripImageUrl: String 8 | tripImageKey: String 9 | Activities: [Activity] @hasMany(indexName: "byTrip", fields: ["id"]) 10 | } 11 | 12 | type Activity @model @auth(rules: [{allow: owner}]) { 13 | id: ID! 14 | activityName: String! 15 | tripID: ID! @index(name: "byTrip", sortKeyFields: ["activityName"]) 16 | trip: Trip! @belongsTo(fields: ["tripID"]) 17 | activityImageUrl: String 18 | activityImageKey: String 19 | activityDate: AWSDate! 20 | activityTime: AWSTime 21 | category: ActivityCategory! 22 | } 23 | 24 | 25 | 26 | type Profile 27 | @model 28 | @auth( 29 | rules: [ 30 | { allow: private, provider: iam } 31 | { allow: owner, operations: [read, update, create] } 32 | ] 33 | ) { 34 | id: ID! 35 | email: String! 36 | firstName: String 37 | lastName: String 38 | homeCity: String 39 | owner: String! 40 | } 41 | 42 | enum ActivityCategory { Flight, Lodging, Meeting, Restaurant } 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/features/trip/ui/past_trips/past_trips_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/features/trip/controller/past_trips_list.dart'; 4 | import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trips_list_gridview.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class PastTripsList extends ConsumerWidget { 9 | const PastTripsList({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final tripsListValue = ref.watch(pastTripsListProvider); 16 | 17 | return Scaffold( 18 | appBar: AppBar( 19 | centerTitle: true, 20 | title: const Text( 21 | 'Amplify Trips Planner', 22 | ), 23 | backgroundColor: const Color(constants.primaryColorDark), 24 | ), 25 | drawer: const TheNavigationDrawer(), 26 | body: TripsListGridView( 27 | tripsList: tripsListValue, 28 | isPast: true, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:amplify_trips_planner/trips_planner_app.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const TripsPlannerApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/trip/data/trips_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/trip/service/trips_api_service.dart'; 2 | import 'package:amplify_trips_planner/models/Trip.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | final tripsRepositoryProvider = Provider((ref) { 6 | final tripsAPIService = ref.read(tripsAPIServiceProvider); 7 | return TripsRepository(tripsAPIService); 8 | }); 9 | 10 | class TripsRepository { 11 | TripsRepository(this.tripsAPIService); 12 | 13 | final TripsAPIService tripsAPIService; 14 | 15 | Future> getTrips() { 16 | return tripsAPIService.getTrips(); 17 | } 18 | 19 | Future> getPastTrips() { 20 | return tripsAPIService.getPastTrips(); 21 | } 22 | 23 | Future add(Trip trip) async { 24 | return tripsAPIService.addTrip(trip); 25 | } 26 | 27 | Future update(Trip updatedTrip) async { 28 | return tripsAPIService.updateTrip(updatedTrip); 29 | } 30 | 31 | Future delete(Trip deletedTrip) async { 32 | return tripsAPIService.deleteTrip(deletedTrip); 33 | } 34 | 35 | Future getTrip(String tripId) async { 36 | return tripsAPIService.getTrip(tripId); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/features/profile/service/profile_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_api/amplify_api.dart'; 2 | import 'package:amplify_flutter/amplify_flutter.dart'; 3 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | final profileAPIServiceProvider = Provider((ref) { 7 | return ProfileAPIService(); 8 | }); 9 | 10 | class ProfileAPIService { 11 | ProfileAPIService(); 12 | 13 | Future getProfile() async { 14 | try { 15 | final request = ModelQueries.list(Profile.classType); 16 | final response = await Amplify.API.query(request: request).response; 17 | 18 | final profile = response.data!.items.first; 19 | 20 | return profile!; 21 | } on Exception catch (error) { 22 | safePrint('getProfile failed: $error'); 23 | rethrow; 24 | } 25 | } 26 | 27 | Future updateProfile(Profile updatedProfile) async { 28 | try { 29 | await Amplify.API 30 | .mutate( 31 | request: ModelMutations.update(updatedProfile), 32 | ) 33 | .response; 34 | } on Exception catch (error) { 35 | safePrint('updateProfile failed: $error'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activity_page/activity_page_appbar_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:go_router/go_router.dart'; 6 | 7 | class ActivityPageAppBarIcon extends StatelessWidget { 8 | const ActivityPageAppBarIcon({ 9 | super.key, 10 | required this.activity, 11 | }); 12 | 13 | final AsyncValue activity; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | switch (activity) { 18 | case AsyncData(:final value): 19 | return IconButton( 20 | onPressed: () { 21 | context.goNamed( 22 | AppRoute.trip.name, 23 | pathParameters: {'id': value.trip.id}, 24 | ); 25 | }, 26 | icon: const Icon(Icons.arrow_back), 27 | ); 28 | 29 | case AsyncError(): 30 | return const Placeholder(); 31 | case AsyncLoading(): 32 | return const SizedBox(); 33 | 34 | case _: 35 | return const Center( 36 | child: Text('Error'), 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/activity/data/activities_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/activity/service/activities_api_service.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | final activitiesRepositoryProvider = Provider((ref) { 6 | final activitiesAPIService = ref.read(activitiesAPIServiceProvider); 7 | return ActivitiesRepository(activitiesAPIService); 8 | }); 9 | 10 | class ActivitiesRepository { 11 | ActivitiesRepository( 12 | this.activitiesAPIService, 13 | ); 14 | 15 | final ActivitiesAPIService activitiesAPIService; 16 | 17 | Future> getActivitiesForTrip(String tripId) { 18 | return activitiesAPIService.getActivitiesForTrip(tripId); 19 | } 20 | 21 | Future getActivity(String activityId) { 22 | return activitiesAPIService.getActivity(activityId); 23 | } 24 | 25 | Future add(Activity activity) async { 26 | return activitiesAPIService.addActivity(activity); 27 | } 28 | 29 | Future delete(Activity activity) async { 30 | return activitiesAPIService.deleteActivity(activity); 31 | } 32 | 33 | Future update(Activity activity) async { 34 | return activitiesAPIService.updateActivity(activity); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activity_page/activity_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 2 | import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart'; 3 | import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_listview.dart'; 4 | import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_page_appbar_icon.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class ActivityPage extends ConsumerWidget { 9 | const ActivityPage({ 10 | required this.activityId, 11 | super.key, 12 | }); 13 | 14 | final String activityId; 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | final activityValue = ref.watch(activityControllerProvider(activityId)); 19 | 20 | return Scaffold( 21 | appBar: AppBar( 22 | centerTitle: true, 23 | title: const Text( 24 | 'Amplify Trips Planner', 25 | ), 26 | leading: ActivityPageAppBarIcon( 27 | activity: activityValue, 28 | ), 29 | backgroundColor: const Color(constants.primaryColorDark), 30 | ), 31 | body: ActivityListView( 32 | activity: activityValue, 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trip_page/trip_page_floating_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | class TripPageFloatingButton extends StatelessWidget { 9 | const TripPageFloatingButton({ 10 | required this.trip, 11 | super.key, 12 | }); 13 | 14 | final AsyncValue trip; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | switch (trip) { 19 | case AsyncData(:final value): 20 | return FloatingActionButton( 21 | onPressed: () { 22 | context.goNamed( 23 | AppRoute.addActivity.name, 24 | pathParameters: {'id': value.id}, 25 | ); 26 | }, 27 | backgroundColor: const Color(constants.primaryColorDark), 28 | child: const Icon(Icons.add), 29 | ); 30 | 31 | case AsyncError(): 32 | return const Placeholder(); 33 | case AsyncLoading(): 34 | return const SizedBox(); 35 | 36 | case _: 37 | return const SizedBox(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activities_list/activities_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/activity/controller/activities_list.dart'; 2 | import 'package:amplify_trips_planner/features/activity/ui/activities_list/activities_timeline.dart'; 3 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | 7 | class ActivitiesList extends ConsumerWidget { 8 | const ActivitiesList({ 9 | required this.trip, 10 | super.key, 11 | }); 12 | final Trip trip; 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final activitiesListValue = ref.watch(activitiesListProvider(trip.id)); 17 | switch (activitiesListValue) { 18 | case AsyncData(:final value): 19 | return value.isEmpty 20 | ? const Center( 21 | child: Text('No Activities'), 22 | ) 23 | : ActivitiesTimeline(activities: value); 24 | 25 | case AsyncError(): 26 | return const Center( 27 | child: Text('Error'), 28 | ); 29 | case AsyncLoading(): 30 | return const Center( 31 | child: CircularProgressIndicator(), 32 | ); 33 | 34 | case _: 35 | return const Center( 36 | child: Text('Error'), 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/src/index.js: -------------------------------------------------------------------------------- 1 | /* Amplify Params - DO NOT EDIT 2 | API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIENDPOINTOUTPUT 3 | API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIIDOUTPUT 4 | ENV 5 | REGION 6 | Amplify Params - DO NOT EDIT *//** 7 | * @fileoverview 8 | * 9 | * This CloudFormation Trigger creates a handler which awaits the other handlers 10 | * specified in the `MODULES` env var, located at `./${MODULE}`. 11 | */ 12 | 13 | /** 14 | * The names of modules to load are stored as a comma-delimited string in the 15 | * `MODULES` env var. 16 | */ 17 | const moduleNames = process.env.MODULES.split(','); 18 | /** 19 | * The array of imported modules. 20 | */ 21 | const modules = moduleNames.map((name) => require(`./${name}`)); 22 | 23 | /** 24 | * This async handler iterates over the given modules and awaits them. 25 | * 26 | * @see https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-handler-async 27 | * @type {import('@types/aws-lambda').APIGatewayProxyHandler} 28 | * 29 | */ 30 | exports.handler = async (event, context) => { 31 | /** 32 | * Instead of naively iterating over all handlers, run them concurrently with 33 | * `await Promise.all(...)`. This would otherwise just be determined by the 34 | * order of names in the `MODULES` var. 35 | */ 36 | await Promise.all(modules.map((module) => module.handler(event, context))); 37 | return event; 38 | }; 39 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trips_gridview/trip_gridview_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trip_gridview_item_card.dart'; 4 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | class TripGridViewItem extends StatelessWidget { 9 | const TripGridViewItem({ 10 | required this.trip, 11 | required this.isPast, 12 | super.key, 13 | }); 14 | 15 | final Trip trip; 16 | final bool isPast; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return InkWell( 21 | splashColor: Theme.of(context).primaryColor, 22 | borderRadius: BorderRadius.circular(15), 23 | onTap: () { 24 | context.goNamed( 25 | isPast ? AppRoute.pastTrip.name : AppRoute.trip.name, 26 | pathParameters: {'id': trip.id}, 27 | extra: trip, 28 | ); 29 | }, 30 | child: isPast 31 | ? ColorFiltered( 32 | colorFilter: const ColorFilter.matrix(constants.greyoutMatrix), 33 | child: TripGridViewItemCard( 34 | trip: trip, 35 | ), 36 | ) 37 | : TripGridViewItemCard( 38 | trip: trip, 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | #amplify-do-not-edit-begin 47 | amplify/\#current-cloud-backend 48 | amplify/.config/local-* 49 | amplify/logs 50 | amplify/mock-data 51 | amplify/mock-api-resources 52 | amplify/backend/amplify-meta.json 53 | amplify/backend/.temp 54 | build/ 55 | dist/ 56 | node_modules/ 57 | aws-exports.js 58 | awsconfiguration.json 59 | amplifyconfiguration.json 60 | amplifyconfiguration.dart 61 | amplify-build-config.json 62 | amplify-gradle-config.json 63 | amplifytools.xcconfig 64 | .secret-* 65 | **.sample 66 | #amplify-do-not-edit-end 67 | lib/test.dart 68 | lib/somthing.md 69 | 70 | team-provider-info.json 71 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.5' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activity_category_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ActivityCategoryIcon extends StatelessWidget { 6 | const ActivityCategoryIcon({ 7 | required this.activityCategory, 8 | super.key, 9 | }); 10 | final ActivityCategory activityCategory; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | switch (activityCategory) { 15 | case ActivityCategory.Flight: 16 | return const Icon( 17 | Icons.flight, 18 | size: 50, 19 | color: Color(constants.primaryColorDark), 20 | ); 21 | 22 | case ActivityCategory.Lodging: 23 | return const Icon( 24 | Icons.hotel, 25 | size: 50, 26 | color: Color(constants.primaryColorDark), 27 | ); 28 | case ActivityCategory.Meeting: 29 | return const Icon( 30 | Icons.computer, 31 | size: 50, 32 | color: Color(constants.primaryColorDark), 33 | ); 34 | case ActivityCategory.Restaurant: 35 | return const Icon( 36 | Icons.restaurant, 37 | size: 50, 38 | color: Color(constants.primaryColorDark), 39 | ); 40 | default: 41 | ActivityCategory.Flight; 42 | } 43 | return const Icon( 44 | Icons.flight, 45 | size: 50, 46 | color: Color(constants.primaryColorDark), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/features/activity/ui/add_activity/add_activity_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_form.dart'; 4 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:go_router/go_router.dart'; 8 | 9 | class AddActivityPage extends ConsumerWidget { 10 | AddActivityPage({ 11 | required this.tripId, 12 | super.key, 13 | }); 14 | 15 | final String tripId; 16 | 17 | final formGlobalKey = GlobalKey(); 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | final tripValue = ref.watch(tripControllerProvider(tripId)); 22 | 23 | return Scaffold( 24 | appBar: AppBar( 25 | centerTitle: true, 26 | title: const Text( 27 | 'Amplify Trips Planner', 28 | ), 29 | leading: IconButton( 30 | onPressed: () { 31 | context.goNamed( 32 | AppRoute.trip.name, 33 | pathParameters: {'id': tripId}, 34 | ); 35 | }, 36 | icon: const Icon(Icons.arrow_back), 37 | ), 38 | backgroundColor: const Color(constants.primaryColorDark), 39 | ), 40 | body: AddActivityForm( 41 | trip: tripValue, 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/ui/upload_progress_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | class UploadProgressDialog extends ConsumerWidget { 6 | const UploadProgressDialog({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context, WidgetRef ref) { 10 | return Dialog( 11 | backgroundColor: Colors.white, 12 | child: Padding( 13 | padding: const EdgeInsets.symmetric(vertical: 20), 14 | child: ValueListenableBuilder( 15 | valueListenable: 16 | ref.read(tripControllerProvider('').notifier).uploadProgress(), 17 | builder: (context, value, child) { 18 | return Column( 19 | mainAxisSize: MainAxisSize.min, 20 | children: [ 21 | const CircularProgressIndicator(), 22 | const SizedBox( 23 | height: 15, 24 | ), 25 | Text('${(double.parse(value.toString()) * 100).toInt()} %'), 26 | Container( 27 | alignment: Alignment.topCenter, 28 | margin: const EdgeInsets.all(20), 29 | child: LinearProgressIndicator( 30 | value: double.parse(value.toString()), 31 | backgroundColor: Colors.grey, 32 | color: Colors.purple, 33 | minHeight: 10, 34 | ), 35 | ), 36 | ], 37 | ); 38 | }, 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/features/trip/ui/past_trip_page/past_trip_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart'; 3 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 4 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 5 | import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/past_trip_details.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | import 'package:go_router/go_router.dart'; 9 | 10 | class PastTripPage extends ConsumerWidget { 11 | const PastTripPage({ 12 | required this.tripId, 13 | super.key, 14 | }); 15 | final String tripId; 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | final tripValue = ref.watch(tripControllerProvider(tripId)); 20 | return Scaffold( 21 | appBar: AppBar( 22 | centerTitle: true, 23 | title: const Text( 24 | 'Amplify Trips Planner', 25 | ), 26 | actions: [ 27 | IconButton( 28 | onPressed: () { 29 | context.goNamed( 30 | AppRoute.home.name, 31 | ); 32 | }, 33 | icon: const Icon(Icons.home), 34 | ), 35 | ], 36 | backgroundColor: const Color(constants.primaryColorDark), 37 | ), 38 | drawer: const TheNavigationDrawer(), 39 | body: ColorFiltered( 40 | colorFilter: const ColorFilter.matrix(constants.greyoutMatrix), 41 | child: PastTripDetails( 42 | trip: tripValue, 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/features/trip/controller/trips_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:amplify_flutter/amplify_flutter.dart'; 4 | import 'package:amplify_trips_planner/features/trip/data/trips_repository.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'trips_list.g.dart'; 9 | 10 | @riverpod 11 | class TripsList extends _$TripsList { 12 | Future> _fetchTrips() async { 13 | final tripsRepository = ref.read(tripsRepositoryProvider); 14 | final trips = await tripsRepository.getTrips(); 15 | return trips; 16 | } 17 | 18 | @override 19 | FutureOr> build() async { 20 | return _fetchTrips(); 21 | } 22 | 23 | Future addTrip({ 24 | required String name, 25 | required String destination, 26 | required String startDate, 27 | required String endDate, 28 | }) async { 29 | final trip = Trip( 30 | tripName: name, 31 | destination: destination, 32 | startDate: TemporalDate(DateTime.parse(startDate)), 33 | endDate: TemporalDate(DateTime.parse(endDate)), 34 | ); 35 | 36 | state = const AsyncValue.loading(); 37 | 38 | state = await AsyncValue.guard(() async { 39 | final tripsRepository = ref.read(tripsRepositoryProvider); 40 | await tripsRepository.add(trip); 41 | return _fetchTrips(); 42 | }); 43 | } 44 | 45 | Future removeTrip(Trip trip) async { 46 | state = const AsyncValue.loading(); 47 | state = await AsyncValue.guard(() async { 48 | final tripsRepository = ref.read(tripsRepositoryProvider); 49 | await tripsRepository.delete(trip); 50 | 51 | return _fetchTrips(); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/models/ActivityCategory.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | // NOTE: This file is generated and may not follow lint rules defined in your app 17 | // Generated files can be excluded from analysis in analysis_options.yaml 18 | // For more info, see: https://dart.dev/guides/language/analysis-options#excluding-code-from-analysis 19 | 20 | // ignore_for_file: public_member_api_docs, annotate_overrides, dead_code, dead_codepublic_member_api_docs, depend_on_referenced_packages, file_names, library_private_types_in_public_api, no_leading_underscores_for_library_prefixes, no_leading_underscores_for_local_identifiers, non_constant_identifier_names, null_check_on_nullable_type_parameter, prefer_adjacent_string_concatenation, prefer_const_constructors, prefer_if_null_operators, prefer_interpolation_to_compose_strings, slash_for_doc_comments, sort_child_properties_last, unnecessary_const, unnecessary_constructor_name, unnecessary_late, unnecessary_new, unnecessary_null_aware_assignments, unnecessary_nullable_for_final_variable_declarations, unnecessary_string_interpolations, use_build_context_synchronously 21 | 22 | enum ActivityCategory { Flight, Lodging, Meeting, Restaurant } 23 | -------------------------------------------------------------------------------- /amplify/backend/api/amplifytripsplanner/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trip_page/trip_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart'; 3 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 4 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 5 | import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_details.dart'; 6 | import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page_floating_button.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | import 'package:go_router/go_router.dart'; 10 | 11 | class TripPage extends ConsumerWidget { 12 | const TripPage({ 13 | required this.tripId, 14 | super.key, 15 | }); 16 | final String tripId; 17 | 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | final tripValue = ref.watch(tripControllerProvider(tripId)); 21 | 22 | return Scaffold( 23 | appBar: AppBar( 24 | centerTitle: true, 25 | title: const Text( 26 | 'Amplify Trips Planner', 27 | ), 28 | actions: [ 29 | IconButton( 30 | onPressed: () { 31 | context.goNamed( 32 | AppRoute.home.name, 33 | ); 34 | }, 35 | icon: const Icon(Icons.home), 36 | ), 37 | ], 38 | backgroundColor: const Color(constants.primaryColorDark), 39 | ), 40 | drawer: const TheNavigationDrawer(), 41 | floatingActionButton: TripPageFloatingButton( 42 | trip: tripValue, 43 | ), 44 | body: TripDetails( 45 | tripId: tripId, 46 | trip: tripValue, 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/features/trip/controller/trip_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:amplify_trips_planner/common/services/storage_service.dart'; 5 | import 'package:amplify_trips_planner/features/trip/data/trips_repository.dart'; 6 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 9 | 10 | part 'trip_controller.g.dart'; 11 | 12 | @riverpod 13 | class TripController extends _$TripController { 14 | Future _fetchTrip(String tripId) async { 15 | final tripsRepository = ref.read(tripsRepositoryProvider); 16 | return tripsRepository.getTrip(tripId); 17 | } 18 | 19 | @override 20 | FutureOr build(String tripId) async { 21 | return _fetchTrip(tripId); 22 | } 23 | 24 | Future updateTrip(Trip trip) async { 25 | state = const AsyncValue.loading(); 26 | state = await AsyncValue.guard(() async { 27 | final tripsRepository = ref.read(tripsRepositoryProvider); 28 | await tripsRepository.update(trip); 29 | return _fetchTrip(trip.id); 30 | }); 31 | } 32 | 33 | Future uploadFile(File file, Trip trip) async { 34 | final fileKey = await ref.read(storageServiceProvider).uploadFile(file); 35 | if (fileKey != null) { 36 | final imageUrl = 37 | await ref.read(storageServiceProvider).getImageUrl(fileKey); 38 | final updatedTrip = 39 | trip.copyWith(tripImageKey: fileKey, tripImageUrl: imageUrl); 40 | await ref.read(tripsRepositoryProvider).update(updatedTrip); 41 | ref.read(storageServiceProvider).resetUploadProgress(); 42 | } 43 | } 44 | 45 | ValueNotifier uploadProgress() { 46 | return ref.read(storageServiceProvider).getUploadProgress(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trips_list/trips_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/ui/the_navigation_drawer.dart'; 2 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 3 | import 'package:amplify_trips_planner/features/trip/controller/trips_list.dart'; 4 | import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trips_list_gridview.dart'; 5 | import 'package:amplify_trips_planner/features/trip/ui/trips_list/add_trip_bottomsheet.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | 9 | class TripsListPage extends ConsumerWidget { 10 | const TripsListPage({ 11 | super.key, 12 | }); 13 | 14 | Future showAddTripDialog(BuildContext context) => 15 | showModalBottomSheet( 16 | isScrollControlled: true, 17 | elevation: 5, 18 | context: context, 19 | builder: (sheetContext) { 20 | return AddTripBottomSheet(); 21 | }, 22 | ); 23 | 24 | @override 25 | Widget build(BuildContext context, WidgetRef ref) { 26 | final tripsListValue = ref.watch(tripsListProvider); 27 | return Scaffold( 28 | appBar: AppBar( 29 | centerTitle: true, 30 | title: const Text( 31 | 'Amplify Trips Planner', 32 | ), 33 | backgroundColor: const Color(constants.primaryColorDark), 34 | ), 35 | drawer: const TheNavigationDrawer(), 36 | floatingActionButton: FloatingActionButton( 37 | onPressed: () { 38 | showAddTripDialog(context); 39 | }, 40 | backgroundColor: const Color(constants.primaryColorDark), 41 | child: const Icon(Icons.add), 42 | ), 43 | body: TripsListGridView( 44 | tripsList: tripsListValue, 45 | isPast: false, 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/common/services/storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:amplify_flutter/amplify_flutter.dart'; 4 | import 'package:amplify_storage_s3/amplify_storage_s3.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:path/path.dart' as p; 8 | import 'package:uuid/uuid.dart'; 9 | 10 | final storageServiceProvider = Provider((ref) { 11 | return StorageService(ref: ref); 12 | }); 13 | 14 | class StorageService { 15 | StorageService({ 16 | required Ref ref, 17 | }); 18 | 19 | ValueNotifier uploadProgress = ValueNotifier(0); 20 | Future getImageUrl(String key) async { 21 | final result = await Amplify.Storage.getUrl( 22 | key: key, 23 | options: const StorageGetUrlOptions( 24 | pluginOptions: S3GetUrlPluginOptions( 25 | validateObjectExistence: true, 26 | expiresIn: Duration(days: 1), 27 | ), 28 | ), 29 | ).result; 30 | return result.url.toString(); 31 | } 32 | 33 | ValueNotifier getUploadProgress() { 34 | return uploadProgress; 35 | } 36 | 37 | Future uploadFile(File file) async { 38 | try { 39 | final extension = p.extension(file.path); 40 | final key = const Uuid().v1() + extension; 41 | final awsFile = AWSFile.fromPath(file.path); 42 | 43 | await Amplify.Storage.uploadFile( 44 | localFile: awsFile, 45 | key: key, 46 | onProgress: (progress) { 47 | uploadProgress.value = progress.fractionCompleted; 48 | }, 49 | ).result; 50 | 51 | return key; 52 | } on Exception catch (e) { 53 | debugPrint(e.toString()); 54 | return null; 55 | } 56 | } 57 | 58 | void resetUploadProgress() { 59 | uploadProgress.value = 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/features/trip/ui/past_trip_page/past_trip_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/selected_past_trip_card.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | class PastTripDetails extends ConsumerWidget { 7 | const PastTripDetails({ 8 | required this.trip, 9 | super.key, 10 | }); 11 | 12 | final AsyncValue trip; 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | switch (trip) { 17 | case AsyncData(:final value): 18 | return Column( 19 | crossAxisAlignment: CrossAxisAlignment.stretch, 20 | children: [ 21 | const SizedBox( 22 | height: 8, 23 | ), 24 | SelectedPastTripCard(trip: value), 25 | const SizedBox( 26 | height: 20, 27 | ), 28 | const Divider( 29 | height: 20, 30 | thickness: 5, 31 | indent: 20, 32 | endIndent: 20, 33 | ), 34 | const Text( 35 | 'Your Activities', 36 | textAlign: TextAlign.center, 37 | style: TextStyle( 38 | fontSize: 20, 39 | fontWeight: FontWeight.bold, 40 | ), 41 | ), 42 | const SizedBox( 43 | height: 8, 44 | ), 45 | ], 46 | ); 47 | 48 | case AsyncError(): 49 | return const Center( 50 | child: Text('Error'), 51 | ); 52 | case AsyncLoading(): 53 | return const Center( 54 | child: CircularProgressIndicator(), 55 | ); 56 | 57 | case _: 58 | return const Center( 59 | child: Text('Error'), 60 | ); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trips_gridview/trips_list_gridview.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/trip/ui/trips_gridview/trip_gridview_item.dart'; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | class TripsListGridView extends StatelessWidget { 7 | const TripsListGridView({ 8 | required this.tripsList, 9 | required this.isPast, 10 | super.key, 11 | }); 12 | 13 | final AsyncValue> tripsList; 14 | final bool isPast; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | switch (tripsList) { 19 | case AsyncData(:final value): 20 | return value.isEmpty 21 | ? const Center( 22 | child: Text('No Trips'), 23 | ) 24 | : OrientationBuilder( 25 | builder: (context, orientation) { 26 | return GridView.count( 27 | crossAxisCount: 28 | (orientation == Orientation.portrait) ? 2 : 3, 29 | mainAxisSpacing: 4, 30 | crossAxisSpacing: 4, 31 | padding: const EdgeInsets.all(4), 32 | childAspectRatio: 33 | (orientation == Orientation.portrait) ? 0.9 : 1.4, 34 | children: value.map((tripData) { 35 | return TripGridViewItem( 36 | trip: tripData, 37 | isPast: isPast, 38 | ); 39 | }).toList(growable: false), 40 | ); 41 | }, 42 | ); 43 | 44 | case AsyncError(): 45 | return const Center( 46 | child: Text('Error'), 47 | ); 48 | case AsyncLoading(): 49 | return const Center( 50 | child: CircularProgressIndicator(), 51 | ); 52 | 53 | case _: 54 | return const Center( 55 | child: Text('Error'), 56 | ); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/features/activity/controller/activity_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:amplify_trips_planner/common/services/storage_service.dart'; 4 | import 'package:amplify_trips_planner/features/activity/data/activities_repository.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'activity_controller.g.dart'; 10 | 11 | @riverpod 12 | class ActivityController extends _$ActivityController { 13 | Future _fetchActivity(String activityId) async { 14 | final activitiesRepository = ref.read(activitiesRepositoryProvider); 15 | return activitiesRepository.getActivity(activityId); 16 | } 17 | 18 | @override 19 | FutureOr build(String activityId) async { 20 | return _fetchActivity(activityId); 21 | } 22 | 23 | Future uploadFile(File file, Activity activity) async { 24 | final fileKey = await ref.read(storageServiceProvider).uploadFile(file); 25 | if (fileKey != null) { 26 | final imageUrl = 27 | await ref.read(storageServiceProvider).getImageUrl(fileKey); 28 | final updatedActivity = activity.copyWith( 29 | activityImageKey: fileKey, 30 | activityImageUrl: imageUrl, 31 | ); 32 | await updateActivity(updatedActivity); 33 | ref.read(storageServiceProvider).resetUploadProgress(); 34 | } 35 | } 36 | 37 | Future getFileUrl(Activity activity) async { 38 | final fileKey = activity.activityImageKey; 39 | 40 | return ref.read(storageServiceProvider).getImageUrl(fileKey!); 41 | } 42 | 43 | ValueNotifier uploadProgress() { 44 | return ref.read(storageServiceProvider).getUploadProgress(); 45 | } 46 | 47 | Future updateActivity(Activity activity) async { 48 | state = const AsyncValue.loading(); 49 | state = await AsyncValue.guard(() async { 50 | final activitiesRepository = ref.read(activitiesRepositoryProvider); 51 | await activitiesRepository.update(activity); 52 | return _fetchActivity(activity.id); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "improvepluralization": false, 6 | "validatetypenamereservedwords": true, 7 | "useexperimentalpipelinedtransformer": true, 8 | "enableiterativegsiupdates": true, 9 | "secondarykeyasgsi": true, 10 | "skipoverridemutationinputtypes": true, 11 | "transformerversion": 2, 12 | "suppressschemamigrationprompt": true, 13 | "securityenhancementnotification": false, 14 | "showfieldauthnotification": false, 15 | "usesubusernamefordefaultidentityclaim": true, 16 | "usefieldnameforprimarykeyconnectionfield": false, 17 | "enableautoindexquerynames": true, 18 | "respectprimarykeyattributesonconnectionfield": true, 19 | "shoulddeepmergedirectiveconfigdefaults": false, 20 | "populateownerfieldforstaticgroupauth": true 21 | }, 22 | "frontend-ios": { 23 | "enablexcodeintegration": true 24 | }, 25 | "auth": { 26 | "enablecaseinsensitivity": true, 27 | "useinclusiveterminology": true, 28 | "breakcirculardependency": true, 29 | "forcealiasattributes": false, 30 | "useenabledmfas": true 31 | }, 32 | "codegen": { 33 | "useappsyncmodelgenplugin": true, 34 | "usedocsgeneratorplugin": true, 35 | "usetypesgeneratorplugin": true, 36 | "cleangeneratedmodelsdirectory": true, 37 | "retaincasestyle": true, 38 | "addtimestampfields": true, 39 | "handlelistnullabilitytransparently": true, 40 | "emitauthprovider": true, 41 | "generateindexrules": true, 42 | "enabledartnullsafety": true, 43 | "generatemodelsforlazyloadandcustomselectionset": false 44 | }, 45 | "appsync": { 46 | "generategraphqlpermissions": true 47 | }, 48 | "latestregionsupport": { 49 | "pinpoint": 1, 50 | "translate": 1, 51 | "transcribe": 1, 52 | "rekognition": 1, 53 | "textract": 1, 54 | "comprehend": 1 55 | }, 56 | "project": { 57 | "overrides": true 58 | } 59 | }, 60 | "debug": { 61 | "shareProjectConfig": false 62 | } 63 | } -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Amplify Trips Planner Tutorial 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | amplify_trips_planner_tutorial 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | NSCameraUsageDescription 51 | Some Description 52 | NSMicrophoneUsageDescription 53 | Some Description 54 | NSPhotoLibraryUsageDescription 55 | Some Description 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/features/trip/ui/past_trip_page/selected_past_trip_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 2 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | 7 | class SelectedPastTripCard extends ConsumerWidget { 8 | const SelectedPastTripCard({ 9 | required this.trip, 10 | super.key, 11 | }); 12 | 13 | final Trip trip; 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | return Card( 18 | clipBehavior: Clip.antiAlias, 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(15), 21 | ), 22 | elevation: 5, 23 | child: Column( 24 | mainAxisSize: MainAxisSize.min, 25 | children: [ 26 | Text( 27 | trip.tripName, 28 | textAlign: TextAlign.center, 29 | style: const TextStyle( 30 | fontSize: 20, 31 | fontWeight: FontWeight.bold, 32 | ), 33 | ), 34 | const SizedBox( 35 | height: 8, 36 | ), 37 | Container( 38 | alignment: Alignment.center, 39 | color: const Color(constants.primaryColorDark), 40 | height: 150, 41 | child: trip.tripImageUrl != null 42 | ? Stack( 43 | children: [ 44 | const Center(child: CircularProgressIndicator()), 45 | CachedNetworkImage( 46 | imageUrl: trip.tripImageUrl!, 47 | cacheKey: trip.tripImageKey, 48 | width: double.maxFinite, 49 | height: 500, 50 | alignment: Alignment.topCenter, 51 | fit: BoxFit.fill, 52 | ), 53 | ], 54 | ) 55 | : Image.asset( 56 | 'images/amplify.png', 57 | fit: BoxFit.contain, 58 | ), 59 | ), 60 | ], 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/features/activity/controller/activities_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:amplify_flutter/amplify_flutter.dart'; 4 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 5 | import 'package:amplify_trips_planner/features/activity/data/activities_repository.dart'; 6 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 9 | 10 | part 'activities_list.g.dart'; 11 | 12 | @riverpod 13 | class ActivitiesList extends _$ActivitiesList { 14 | Future> _fetchActivities(String tripId) async { 15 | final activitiesRepository = ref.read(activitiesRepositoryProvider); 16 | final activities = await activitiesRepository.getActivitiesForTrip(tripId); 17 | return activities; 18 | } 19 | 20 | @override 21 | FutureOr> build(String tripId) async { 22 | return _fetchActivities(tripId); 23 | } 24 | 25 | Future removeActivity(Activity activity) async { 26 | state = const AsyncValue.loading(); 27 | state = await AsyncValue.guard(() async { 28 | final activitiesRepository = ref.read(activitiesRepositoryProvider); 29 | await activitiesRepository.delete(activity); 30 | 31 | return _fetchActivities(activity.trip.id); 32 | }); 33 | } 34 | 35 | Future add({ 36 | required String name, 37 | required String activityDate, 38 | required TimeOfDay activityTime, 39 | required ActivityCategory category, 40 | required Trip trip, 41 | }) async { 42 | final now = DateTime.now(); 43 | final time = DateTime( 44 | now.year, 45 | now.month, 46 | now.day, 47 | activityTime.hour, 48 | activityTime.minute, 49 | ); 50 | 51 | final activity = Activity( 52 | activityName: name, 53 | activityDate: TemporalDate(DateTime.parse(activityDate)), 54 | activityTime: TemporalTime.fromString(time.format('HH:mm:ss.sss')), 55 | trip: trip, 56 | category: category, 57 | ); 58 | state = const AsyncValue.loading(); 59 | state = await AsyncValue.guard(() async { 60 | final activitiesRepository = ref.read(activitiesRepositoryProvider); 61 | 62 | await activitiesRepository.add(activity); 63 | 64 | return _fetchActivities(trip.id); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/common/ui/the_navigation_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/services/auth_service.dart'; 3 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | class TheNavigationDrawer extends ConsumerWidget { 9 | const TheNavigationDrawer({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | return Drawer( 16 | child: ListView( 17 | padding: EdgeInsets.zero, 18 | children: [ 19 | const DrawerHeader( 20 | decoration: BoxDecoration( 21 | color: Color(constants.primaryColorDark), 22 | ), 23 | padding: EdgeInsets.all(16), 24 | child: Column( 25 | children: [ 26 | SizedBox(height: 10), 27 | Text( 28 | 'Amplify Trips Planner', 29 | style: TextStyle(fontSize: 22, color: Colors.white), 30 | ), 31 | ], 32 | ), 33 | ), 34 | ListTile( 35 | leading: const Icon(Icons.home), 36 | title: const Text('Trips'), 37 | onTap: () { 38 | context.goNamed( 39 | AppRoute.home.name, 40 | ); 41 | }, 42 | ), 43 | ListTile( 44 | leading: const Icon(Icons.category), 45 | title: const Text('Past Trips'), 46 | onTap: () { 47 | context.goNamed( 48 | AppRoute.pastTrips.name, 49 | ); 50 | }, 51 | ), 52 | ListTile( 53 | leading: const Icon(Icons.settings), 54 | title: const Text('Settings'), 55 | onTap: () { 56 | context.goNamed( 57 | AppRoute.profile.name, 58 | ); 59 | }, 60 | ), 61 | ListTile( 62 | leading: const Icon(Icons.exit_to_app), 63 | title: const Text('Logout'), 64 | onTap: () => ref.read(authServiceProvider).signOut(), 65 | ), 66 | ], 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /amplify/backend/function/amplifytripsplanner4600aecaPostConfirmation/src/custom.js: -------------------------------------------------------------------------------- 1 | const { Sha256 } = require("@aws-crypto/sha256-js"); 2 | const { defaultProvider } = require("@aws-sdk/credential-provider-node"); 3 | const { SignatureV4 } = require("@aws-sdk/signature-v4"); 4 | const { HttpRequest } = require("@aws-sdk/protocol-http"); 5 | const { default: fetch, Request } = require("node-fetch"); 6 | 7 | const GRAPHQL_ENDPOINT = process.env.API_AMPLIFYTRIPSPLANNER_GRAPHQLAPIENDPOINTOUTPUT; 8 | const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; 9 | 10 | const query = /* GraphQL */ ` 11 | mutation createProfile($email: String!,$owner: String!) { 12 | createProfile(input: { 13 | email: $email, 14 | owner: $owner, 15 | 16 | }) { 17 | email 18 | } 19 | } 20 | `; 21 | 22 | 23 | 24 | 25 | /** 26 | * @type {import('@types/aws-lambda').PostConfirmationTriggerHandler} 27 | */ 28 | exports.handler = async (event) => { 29 | console.log(`EVENT: ${JSON.stringify(event)}`); 30 | 31 | 32 | const variables = { 33 | 34 | email: event.request.userAttributes.email, 35 | owner: `${event.request.userAttributes.sub}::${event.userName}` 36 | 37 | }; 38 | 39 | 40 | 41 | 42 | 43 | 44 | const endpoint = new URL(GRAPHQL_ENDPOINT); 45 | 46 | const signer = new SignatureV4({ 47 | credentials: defaultProvider(), 48 | region: AWS_REGION, 49 | service: 'appsync', 50 | sha256: Sha256 51 | }); 52 | 53 | const requestToBeSigned = new HttpRequest({ 54 | method: 'POST', 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | host: endpoint.host 58 | }, 59 | hostname: endpoint.host, 60 | body: JSON.stringify({ query, variables }), 61 | path: endpoint.pathname 62 | }); 63 | 64 | const signed = await signer.sign(requestToBeSigned); 65 | const request = new Request(endpoint, signed); 66 | 67 | let statusCode = 200; 68 | let body; 69 | let response; 70 | 71 | try { 72 | response = await fetch(request); 73 | body = await response.json(); 74 | if (body.errors) statusCode = 400; 75 | } catch (error) { 76 | statusCode = 500; 77 | body = { 78 | errors: [ 79 | { 80 | message: error.message 81 | } 82 | ] 83 | }; 84 | } 85 | 86 | console.log(`statusCode: ${statusCode}`); 87 | console.log(`body: ${JSON.stringify(body)}`); 88 | 89 | return { 90 | statusCode, 91 | body: JSON.stringify(body) 92 | }; 93 | }; -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.amplify_trips_planner" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 50 | minSdkVersion 24 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trip_page/trip_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/features/activity/ui/activities_list/activities_list.dart'; 2 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 3 | import 'package:amplify_trips_planner/features/trip/ui/trip_page/selected_trip_card.dart'; 4 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class TripDetails extends ConsumerWidget { 9 | const TripDetails({ 10 | required this.trip, 11 | required this.tripId, 12 | super.key, 13 | }); 14 | 15 | final AsyncValue trip; 16 | final String tripId; 17 | 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | switch (trip) { 21 | case AsyncData(:final value): 22 | return Column( 23 | crossAxisAlignment: CrossAxisAlignment.stretch, 24 | children: [ 25 | const SizedBox( 26 | height: 8, 27 | ), 28 | SelectedTripCard(trip: value), 29 | const SizedBox( 30 | height: 20, 31 | ), 32 | const Divider( 33 | height: 20, 34 | thickness: 5, 35 | indent: 20, 36 | endIndent: 20, 37 | ), 38 | const Text( 39 | 'Your Activities', 40 | textAlign: TextAlign.center, 41 | style: TextStyle( 42 | fontSize: 20, 43 | fontWeight: FontWeight.bold, 44 | ), 45 | ), 46 | const SizedBox( 47 | height: 8, 48 | ), 49 | Expanded( 50 | child: ActivitiesList( 51 | trip: value, 52 | ), 53 | ) 54 | ], 55 | ); 56 | 57 | case AsyncError(): 58 | return Center( 59 | child: Column( 60 | children: [ 61 | const Text('Error'), 62 | TextButton( 63 | onPressed: () async { 64 | ref.invalidate(tripControllerProvider(tripId)); 65 | }, 66 | child: const Text('Try again'), 67 | ), 68 | ], 69 | ), 70 | ); 71 | case AsyncLoading(): 72 | return const Center( 73 | child: CircularProgressIndicator(), 74 | ); 75 | 76 | case _: 77 | return const Center( 78 | child: Text('Error'), 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /amplify/backend/auth/amplifytripsplanner4600aeca/cli-inputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "cognitoConfig": { 4 | "identityPoolName": "amplifytripsplanner4600aeca_identitypool_4600aeca", 5 | "allowUnauthenticatedIdentities": false, 6 | "resourceNameTruncated": "amplif4600aeca", 7 | "userPoolName": "amplifytripsplanner4600aeca_userpool_4600aeca", 8 | "autoVerifiedAttributes": [ 9 | "email" 10 | ], 11 | "mfaConfiguration": "OFF", 12 | "mfaTypes": [ 13 | "SMS Text Message" 14 | ], 15 | "smsAuthenticationMessage": "Your authentication code is {####}", 16 | "smsVerificationMessage": "Your verification code is {####}", 17 | "emailVerificationSubject": "Your verification code", 18 | "emailVerificationMessage": "Your verification code is {####}", 19 | "defaultPasswordPolicy": false, 20 | "passwordPolicyMinLength": 8, 21 | "passwordPolicyCharacters": [], 22 | "requiredAttributes": [ 23 | "email" 24 | ], 25 | "aliasAttributes": [], 26 | "userpoolClientGenerateSecret": false, 27 | "userpoolClientRefreshTokenValidity": 30, 28 | "userpoolClientWriteAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientReadAttributes": [ 32 | "email" 33 | ], 34 | "userpoolClientLambdaRole": "amplif4600aeca_userpoolclient_lambda_role", 35 | "userpoolClientSetAttributes": false, 36 | "sharedId": "4600aeca", 37 | "resourceName": "amplifytripsplanner4600aeca", 38 | "authSelections": "identityPoolAndUserPool", 39 | "useDefault": "manual", 40 | "usernameAttributes": [ 41 | "email" 42 | ], 43 | "userPoolGroupList": [], 44 | "serviceName": "Cognito", 45 | "usernameCaseSensitive": false, 46 | "useEnabledMfas": true, 47 | "authRoleArn": { 48 | "Fn::GetAtt": [ 49 | "AuthRole", 50 | "Arn" 51 | ] 52 | }, 53 | "unauthRoleArn": { 54 | "Fn::GetAtt": [ 55 | "UnauthRole", 56 | "Arn" 57 | ] 58 | }, 59 | "breakCircularDependency": true, 60 | "dependsOn": [ 61 | { 62 | "category": "function", 63 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation", 64 | "triggerProvider": "Cognito", 65 | "attributes": [ 66 | "Arn", 67 | "Name" 68 | ] 69 | } 70 | ], 71 | "thirdPartyAuth": false, 72 | "userPoolGroups": false, 73 | "adminQueries": false, 74 | "triggers": { 75 | "PostConfirmation": [ 76 | "custom" 77 | ] 78 | }, 79 | "hostedUI": false, 80 | "authProviders": [], 81 | "parentStack": { 82 | "Ref": "AWS::StackId" 83 | }, 84 | "authTriggerConnections": "[\n {\n \"triggerType\": \"PostConfirmation\",\n \"lambdaFunctionName\": \"amplifytripsplanner4600aecaPostConfirmation\"\n }\n]", 85 | "permissions": [] 86 | } 87 | } -------------------------------------------------------------------------------- /lib/models/ModelProvider.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | // NOTE: This file is generated and may not follow lint rules defined in your app 17 | // Generated files can be excluded from analysis in analysis_options.yaml 18 | // For more info, see: https://dart.dev/guides/language/analysis-options#excluding-code-from-analysis 19 | 20 | // ignore_for_file: public_member_api_docs, annotate_overrides, dead_code, dead_codepublic_member_api_docs, depend_on_referenced_packages, file_names, library_private_types_in_public_api, no_leading_underscores_for_library_prefixes, no_leading_underscores_for_local_identifiers, non_constant_identifier_names, null_check_on_nullable_type_parameter, prefer_adjacent_string_concatenation, prefer_const_constructors, prefer_if_null_operators, prefer_interpolation_to_compose_strings, slash_for_doc_comments, sort_child_properties_last, unnecessary_const, unnecessary_constructor_name, unnecessary_late, unnecessary_new, unnecessary_null_aware_assignments, unnecessary_nullable_for_final_variable_declarations, unnecessary_string_interpolations, use_build_context_synchronously 21 | 22 | import 'package:amplify_core/amplify_core.dart'; 23 | import 'Activity.dart'; 24 | import 'Profile.dart'; 25 | import 'Trip.dart'; 26 | 27 | export 'Activity.dart'; 28 | export 'ActivityCategory.dart'; 29 | export 'Profile.dart'; 30 | export 'Trip.dart'; 31 | 32 | class ModelProvider implements ModelProviderInterface { 33 | @override 34 | String version = "dd0adad0d3bb0b223c3b4be681aea544"; 35 | @override 36 | List modelSchemas = [ 37 | Activity.schema, 38 | Profile.schema, 39 | Trip.schema 40 | ]; 41 | static final ModelProvider _instance = ModelProvider(); 42 | @override 43 | List customTypeSchemas = []; 44 | 45 | static ModelProvider get instance => _instance; 46 | 47 | ModelType getModelTypeByModelName(String modelName) { 48 | switch (modelName) { 49 | case "Activity": 50 | return Activity.classType; 51 | case "Profile": 52 | return Profile.classType; 53 | case "Trip": 54 | return Trip.classType; 55 | default: 56 | throw Exception( 57 | "Failed to find model in model provider for model name: " + 58 | modelName); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/features/activity/service/activities_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:amplify_api/amplify_api.dart'; 4 | import 'package:amplify_flutter/amplify_flutter.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | final activitiesAPIServiceProvider = Provider((ref) { 9 | final service = ActivitiesAPIService(); 10 | return service; 11 | }); 12 | 13 | class ActivitiesAPIService { 14 | ActivitiesAPIService(); 15 | 16 | Future> getActivitiesForTrip(String tripId) async { 17 | try { 18 | final request = ModelQueries.list( 19 | Activity.classType, 20 | where: Activity.TRIP.eq(tripId), 21 | ); 22 | 23 | final response = await Amplify.API.query(request: request).response; 24 | 25 | final activites = response.data?.items; 26 | if (activites == null) { 27 | safePrint('errors: ${response.errors}'); 28 | return const []; 29 | } 30 | activites.sort( 31 | (a, b) => a!.activityDate 32 | .getDateTime() 33 | .compareTo(b!.activityDate.getDateTime()), 34 | ); 35 | return activites.map((e) => e as Activity).toList(); 36 | } on Exception catch (error) { 37 | safePrint('getActivitiesForTrip failed: $error'); 38 | return const []; 39 | } 40 | } 41 | 42 | Future addActivity(Activity activity) async { 43 | try { 44 | final request = ModelMutations.create(activity); 45 | final response = await Amplify.API.mutate(request: request).response; 46 | 47 | final createdActivity = response.data; 48 | if (createdActivity == null) { 49 | safePrint('errors: ${response.errors}'); 50 | return; 51 | } 52 | } on Exception catch (error) { 53 | safePrint('addActivity failed: $error'); 54 | } 55 | } 56 | 57 | Future deleteActivity(Activity activity) async { 58 | try { 59 | await Amplify.API 60 | .mutate( 61 | request: ModelMutations.delete(activity), 62 | ) 63 | .response; 64 | } on Exception catch (error) { 65 | safePrint('deleteActivity failed: $error'); 66 | } 67 | } 68 | 69 | Future updateActivity(Activity updatedActivity) async { 70 | try { 71 | await Amplify.API 72 | .mutate( 73 | request: ModelMutations.update(updatedActivity), 74 | ) 75 | .response; 76 | } on Exception catch (error) { 77 | safePrint('updateActivity failed: $error'); 78 | } 79 | } 80 | 81 | Future getActivity(String activityId) async { 82 | try { 83 | final request = ModelQueries.get( 84 | Activity.classType, 85 | ActivityModelIdentifier(id: activityId), 86 | ); 87 | final response = await Amplify.API.query(request: request).response; 88 | 89 | final activity = response.data!; 90 | return activity; 91 | } on Exception catch (error) { 92 | safePrint('getActivity failed: $error'); 93 | rethrow; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activities_list/activities_timeline.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 3 | import 'package:amplify_trips_planner/features/activity/ui/activity_category_icon.dart'; 4 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | import 'package:timelines/timelines.dart'; 8 | 9 | class ActivitiesTimeline extends StatelessWidget { 10 | const ActivitiesTimeline({ 11 | super.key, 12 | required this.activities, 13 | }); 14 | 15 | final List activities; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | children: [ 21 | Flexible( 22 | child: Timeline.tileBuilder( 23 | builder: TimelineTileBuilder.fromStyle( 24 | oppositeContentsBuilder: (context, index) { 25 | return InkWell( 26 | onTap: () => context.goNamed( 27 | AppRoute.activity.name, 28 | pathParameters: {'id': activities[index].id}, 29 | ), 30 | child: Padding( 31 | padding: const EdgeInsets.only(bottom: 15), 32 | child: ActivityCategoryIcon( 33 | activityCategory: activities[index].category, 34 | ), 35 | ), 36 | ); 37 | }, 38 | contentsAlign: ContentsAlign.alternating, 39 | contentsBuilder: (context, index) => InkWell( 40 | onTap: () => context.goNamed( 41 | AppRoute.activity.name, 42 | pathParameters: {'id': activities[index].id}, 43 | ), 44 | child: Padding( 45 | padding: const EdgeInsets.all(24), 46 | child: Column( 47 | children: [ 48 | Text( 49 | activities[index].activityName, 50 | style: Theme.of(context).textTheme.titleMedium, 51 | textAlign: TextAlign.center, 52 | ), 53 | const SizedBox( 54 | height: 5, 55 | ), 56 | Text( 57 | activities[index] 58 | .activityDate 59 | .getDateTime() 60 | .format('yyyy-MM-dd'), 61 | style: Theme.of(context).textTheme.bodySmall, 62 | ), 63 | Text( 64 | activities[index] 65 | .activityTime! 66 | .getDateTime() 67 | .format('hh:mm a'), 68 | style: Theme.of(context).textTheme.bodySmall, 69 | ), 70 | ], 71 | ), 72 | ), 73 | ), 74 | itemCount: activities.length, 75 | ), 76 | ), 77 | ), 78 | ], 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "amplifytripsplanner": { 4 | "dependsOn": [ 5 | { 6 | "attributes": [ 7 | "UserPoolId" 8 | ], 9 | "category": "auth", 10 | "resourceName": "amplifytripsplanner4600aeca" 11 | } 12 | ], 13 | "output": { 14 | "authConfig": { 15 | "additionalAuthenticationProviders": [ 16 | { 17 | "authenticationType": "AWS_IAM" 18 | } 19 | ], 20 | "defaultAuthentication": { 21 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 22 | "userPoolConfig": { 23 | "userPoolId": "authamplifytripsplanner4600aeca" 24 | } 25 | } 26 | } 27 | }, 28 | "providerPlugin": "awscloudformation", 29 | "service": "AppSync" 30 | } 31 | }, 32 | "auth": { 33 | "amplifytripsplanner4600aeca": { 34 | "customAuth": false, 35 | "dependsOn": [ 36 | { 37 | "attributes": [ 38 | "Arn", 39 | "Name" 40 | ], 41 | "category": "function", 42 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation", 43 | "triggerProvider": "Cognito" 44 | } 45 | ], 46 | "frontendAuthConfig": { 47 | "mfaConfiguration": "OFF", 48 | "mfaTypes": [ 49 | "SMS" 50 | ], 51 | "passwordProtectionSettings": { 52 | "passwordPolicyCharacters": [], 53 | "passwordPolicyMinLength": 8 54 | }, 55 | "signupAttributes": [ 56 | "EMAIL" 57 | ], 58 | "socialProviders": [], 59 | "usernameAttributes": [ 60 | "EMAIL" 61 | ], 62 | "verificationMechanisms": [ 63 | "EMAIL" 64 | ] 65 | }, 66 | "providerPlugin": "awscloudformation", 67 | "service": "Cognito" 68 | } 69 | }, 70 | "function": { 71 | "amplifytripsplanner4600aecaPostConfirmation": { 72 | "build": true, 73 | "dependsOn": [ 74 | { 75 | "attributes": [ 76 | "GraphQLAPIIdOutput", 77 | "GraphQLAPIEndpointOutput" 78 | ], 79 | "category": "api", 80 | "resourceName": "amplifytripsplanner" 81 | } 82 | ], 83 | "providerPlugin": "awscloudformation", 84 | "service": "Lambda" 85 | } 86 | }, 87 | "parameters": { 88 | "AMPLIFY_function_amplifytripsplanner4600aecaPostConfirmation_deploymentBucketName": { 89 | "usedBy": [ 90 | { 91 | "category": "function", 92 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation" 93 | } 94 | ] 95 | }, 96 | "AMPLIFY_function_amplifytripsplanner4600aecaPostConfirmation_s3Key": { 97 | "usedBy": [ 98 | { 99 | "category": "function", 100 | "resourceName": "amplifytripsplanner4600aecaPostConfirmation" 101 | } 102 | ] 103 | } 104 | }, 105 | "storage": { 106 | "s37d9e620f": { 107 | "dependsOn": [], 108 | "providerPlugin": "awscloudformation", 109 | "service": "S3" 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /lib/common/navigation/router/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/features/activity/ui/activity_page/activity_page.dart'; 3 | import 'package:amplify_trips_planner/features/activity/ui/add_activity/add_activity_page.dart'; 4 | import 'package:amplify_trips_planner/features/activity/ui/edit_activity/edit_activity_page.dart'; 5 | import 'package:amplify_trips_planner/features/profile/ui/profile_page/profile_page.dart'; 6 | import 'package:amplify_trips_planner/features/trip/ui/edit_trip_page/edit_trip_page.dart'; 7 | import 'package:amplify_trips_planner/features/trip/ui/past_trip_page/past_trip_page.dart'; 8 | import 'package:amplify_trips_planner/features/trip/ui/past_trips/past_trips_list.dart'; 9 | import 'package:amplify_trips_planner/features/trip/ui/trip_page/trip_page.dart'; 10 | import 'package:amplify_trips_planner/features/trip/ui/trips_list/trips_list_page.dart'; 11 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:go_router/go_router.dart'; 14 | 15 | final router = GoRouter( 16 | routes: [ 17 | GoRoute( 18 | path: '/', 19 | name: AppRoute.home.name, 20 | builder: (context, state) => const TripsListPage(), 21 | ), 22 | GoRoute( 23 | path: '/trip/:id', 24 | name: AppRoute.trip.name, 25 | builder: (context, state) { 26 | final tripId = state.pathParameters['id']!; 27 | return TripPage(tripId: tripId); 28 | }, 29 | ), 30 | GoRoute( 31 | path: '/edittrip/:id', 32 | name: AppRoute.editTrip.name, 33 | builder: (context, state) { 34 | return EditTripPage( 35 | trip: state.extra! as Trip, 36 | ); 37 | }, 38 | ), 39 | GoRoute( 40 | path: '/pasttrips', 41 | name: AppRoute.pastTrips.name, 42 | builder: (context, state) => const PastTripsList(), 43 | ), 44 | GoRoute( 45 | path: '/pasttrip/:id', 46 | name: AppRoute.pastTrip.name, 47 | builder: (context, state) { 48 | final tripId = state.pathParameters['id']!; 49 | return PastTripPage(tripId: tripId); 50 | }, 51 | ), 52 | GoRoute( 53 | path: '/addActivity/:id', 54 | name: AppRoute.addActivity.name, 55 | builder: (context, state) { 56 | final tripId = state.pathParameters['id']!; 57 | return AddActivityPage(tripId: tripId); 58 | }, 59 | ), 60 | GoRoute( 61 | path: '/activity/:id', 62 | name: AppRoute.activity.name, 63 | builder: (context, state) { 64 | final activityId = state.pathParameters['id']!; 65 | return ActivityPage(activityId: activityId); 66 | }, 67 | ), 68 | GoRoute( 69 | path: '/editactivity/:id', 70 | name: AppRoute.editActivity.name, 71 | builder: (context, state) { 72 | return EditActivityPage( 73 | activity: state.extra! as Activity, 74 | ); 75 | }, 76 | ), 77 | GoRoute( 78 | path: '/profile', 79 | name: AppRoute.profile.name, 80 | builder: (context, state) => const ProfilePage(), 81 | ), 82 | ], 83 | errorBuilder: (context, state) => Scaffold( 84 | body: Center( 85 | child: Text(state.error.toString()), 86 | ), 87 | ), 88 | ); 89 | -------------------------------------------------------------------------------- /lib/features/profile/ui/profile_page/edit_profile_bottomsheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart'; 2 | import 'package:amplify_trips_planner/features/profile/controller/profile_controller.dart'; 3 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | class EditProfileBottomSheet extends ConsumerWidget { 9 | EditProfileBottomSheet({ 10 | required this.profile, 11 | super.key, 12 | }); 13 | 14 | final Profile profile; 15 | 16 | final formGlobalKey = GlobalKey(); 17 | 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | final firstNameController = TextEditingController( 21 | text: profile.firstName != null ? profile.firstName! : '', 22 | ); 23 | final lastNameController = TextEditingController( 24 | text: profile.lastName != null ? profile.lastName! : '', 25 | ); 26 | final homeCityController = TextEditingController( 27 | text: profile.homeCity != null ? profile.homeCity! : '', 28 | ); 29 | 30 | return Form( 31 | key: formGlobalKey, 32 | child: Container( 33 | padding: EdgeInsets.only( 34 | top: 15, 35 | left: 15, 36 | right: 15, 37 | bottom: MediaQuery.of(context).viewInsets.bottom + 15, 38 | ), 39 | width: double.infinity, 40 | child: Column( 41 | mainAxisSize: MainAxisSize.min, 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | BottomSheetTextFormField( 45 | labelText: 'First Name', 46 | controller: firstNameController, 47 | keyboardType: TextInputType.name, 48 | ), 49 | const SizedBox( 50 | height: 20, 51 | ), 52 | BottomSheetTextFormField( 53 | labelText: 'Last Name', 54 | controller: lastNameController, 55 | keyboardType: TextInputType.name, 56 | ), 57 | const SizedBox( 58 | height: 20, 59 | ), 60 | BottomSheetTextFormField( 61 | labelText: 'Home City', 62 | controller: homeCityController, 63 | keyboardType: TextInputType.name, 64 | ), 65 | const SizedBox( 66 | height: 20, 67 | ), 68 | TextButton( 69 | child: const Text('OK'), 70 | onPressed: () async { 71 | final currentState = formGlobalKey.currentState; 72 | if (currentState == null) { 73 | return; 74 | } 75 | if (currentState.validate()) { 76 | final updatedProfile = profile.copyWith( 77 | firstName: firstNameController.text, 78 | lastName: lastNameController.text, 79 | homeCity: homeCityController.text, 80 | ); 81 | await ref 82 | .watch(profileControllerProvider.notifier) 83 | .updateProfile(updatedProfile); 84 | 85 | if (context.mounted) { 86 | context.pop(); 87 | } 88 | } 89 | }, //, 90 | ), 91 | ], 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/features/trip/service/trips_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:amplify_api/amplify_api.dart'; 4 | import 'package:amplify_flutter/amplify_flutter.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | final tripsAPIServiceProvider = Provider((ref) { 9 | final service = TripsAPIService(); 10 | return service; 11 | }); 12 | 13 | class TripsAPIService { 14 | TripsAPIService(); 15 | 16 | Future> getTrips() async { 17 | try { 18 | final request = ModelQueries.list(Trip.classType); 19 | final response = await Amplify.API.query(request: request).response; 20 | 21 | final trips = response.data?.items; 22 | if (trips == null) { 23 | safePrint('getTrips errors: ${response.errors}'); 24 | return const []; 25 | } 26 | trips.sort( 27 | (a, b) => 28 | a!.startDate.getDateTime().compareTo(b!.startDate.getDateTime()), 29 | ); 30 | return trips 31 | .map((e) => e as Trip) 32 | .where( 33 | (element) => element.endDate.getDateTime().isAfter(DateTime.now()), 34 | ) 35 | .toList(); 36 | } on Exception catch (error) { 37 | safePrint('getTrips failed: $error'); 38 | 39 | return const []; 40 | } 41 | } 42 | 43 | Future> getPastTrips() async { 44 | try { 45 | final request = ModelQueries.list(Trip.classType); 46 | final response = await Amplify.API.query(request: request).response; 47 | 48 | final trips = response.data?.items; 49 | if (trips == null) { 50 | safePrint('getPastTrips errors: ${response.errors}'); 51 | return const []; 52 | } 53 | trips.sort( 54 | (a, b) => 55 | a!.startDate.getDateTime().compareTo(b!.startDate.getDateTime()), 56 | ); 57 | return trips 58 | .map((e) => e as Trip) 59 | .where( 60 | (element) => element.endDate.getDateTime().isBefore(DateTime.now()), 61 | ) 62 | .toList(); 63 | } on Exception catch (error) { 64 | safePrint('getPastTrips failed: $error'); 65 | 66 | return const []; 67 | } 68 | } 69 | 70 | Future addTrip(Trip trip) async { 71 | try { 72 | final request = ModelMutations.create(trip); 73 | final response = await Amplify.API.mutate(request: request).response; 74 | 75 | final createdTrip = response.data; 76 | if (createdTrip == null) { 77 | safePrint('addTrip errors: ${response.errors}'); 78 | return; 79 | } 80 | } on Exception catch (error) { 81 | safePrint('addTrip failed: $error'); 82 | } 83 | } 84 | 85 | Future deleteTrip(Trip trip) async { 86 | try { 87 | await Amplify.API 88 | .mutate( 89 | request: ModelMutations.delete(trip), 90 | ) 91 | .response; 92 | } on Exception catch (error) { 93 | safePrint('deleteTrip failed: $error'); 94 | } 95 | } 96 | 97 | Future updateTrip(Trip updatedTrip) async { 98 | try { 99 | await Amplify.API 100 | .mutate( 101 | request: ModelMutations.update(updatedTrip), 102 | ) 103 | .response; 104 | } on Exception catch (error) { 105 | safePrint('updateTrip failed: $error'); 106 | } 107 | } 108 | 109 | Future getTrip(String tripId) async { 110 | try { 111 | final request = ModelQueries.get( 112 | Trip.classType, 113 | TripModelIdentifier(id: tripId), 114 | ); 115 | final response = await Amplify.API.query(request: request).response; 116 | 117 | final trip = response.data!; 118 | return trip; 119 | } on Exception catch (error) { 120 | safePrint('getTrip failed: $error'); 121 | rethrow; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/features/trip/controller/trip_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'trip_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$tripControllerHash() => r'324b8d6d419ebd4dcd2b98c95d80563efee596b2'; 10 | 11 | /// Copied from Dart SDK 12 | class _SystemHash { 13 | _SystemHash._(); 14 | 15 | static int combine(int hash, int value) { 16 | // ignore: parameter_assignments 17 | hash = 0x1fffffff & (hash + value); 18 | // ignore: parameter_assignments 19 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 20 | return hash ^ (hash >> 6); 21 | } 22 | 23 | static int finish(int hash) { 24 | // ignore: parameter_assignments 25 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 26 | // ignore: parameter_assignments 27 | hash = hash ^ (hash >> 11); 28 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 29 | } 30 | } 31 | 32 | abstract class _$TripController 33 | extends BuildlessAutoDisposeAsyncNotifier { 34 | late final String tripId; 35 | 36 | FutureOr build( 37 | String tripId, 38 | ); 39 | } 40 | 41 | /// See also [TripController]. 42 | @ProviderFor(TripController) 43 | const tripControllerProvider = TripControllerFamily(); 44 | 45 | /// See also [TripController]. 46 | class TripControllerFamily extends Family> { 47 | /// See also [TripController]. 48 | const TripControllerFamily(); 49 | 50 | /// See also [TripController]. 51 | TripControllerProvider call( 52 | String tripId, 53 | ) { 54 | return TripControllerProvider( 55 | tripId, 56 | ); 57 | } 58 | 59 | @override 60 | TripControllerProvider getProviderOverride( 61 | covariant TripControllerProvider provider, 62 | ) { 63 | return call( 64 | provider.tripId, 65 | ); 66 | } 67 | 68 | static const Iterable? _dependencies = null; 69 | 70 | @override 71 | Iterable? get dependencies => _dependencies; 72 | 73 | static const Iterable? _allTransitiveDependencies = null; 74 | 75 | @override 76 | Iterable? get allTransitiveDependencies => 77 | _allTransitiveDependencies; 78 | 79 | @override 80 | String? get name => r'tripControllerProvider'; 81 | } 82 | 83 | /// See also [TripController]. 84 | class TripControllerProvider 85 | extends AutoDisposeAsyncNotifierProviderImpl { 86 | /// See also [TripController]. 87 | TripControllerProvider( 88 | this.tripId, 89 | ) : super.internal( 90 | () => TripController()..tripId = tripId, 91 | from: tripControllerProvider, 92 | name: r'tripControllerProvider', 93 | debugGetCreateSourceHash: 94 | const bool.fromEnvironment('dart.vm.product') 95 | ? null 96 | : _$tripControllerHash, 97 | dependencies: TripControllerFamily._dependencies, 98 | allTransitiveDependencies: 99 | TripControllerFamily._allTransitiveDependencies, 100 | ); 101 | 102 | final String tripId; 103 | 104 | @override 105 | bool operator ==(Object other) { 106 | return other is TripControllerProvider && other.tripId == tripId; 107 | } 108 | 109 | @override 110 | int get hashCode { 111 | var hash = _SystemHash.combine(0, runtimeType.hashCode); 112 | hash = _SystemHash.combine(hash, tripId.hashCode); 113 | 114 | return _SystemHash.finish(hash); 115 | } 116 | 117 | @override 118 | FutureOr runNotifierBuild( 119 | covariant TripController notifier, 120 | ) { 121 | return notifier.build( 122 | tripId, 123 | ); 124 | } 125 | } 126 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 127 | -------------------------------------------------------------------------------- /lib/features/activity/controller/activities_list.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'activities_list.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$activitiesListHash() => r'fd5b3964748da9d27e248112fab4ecf2dda9adeb'; 10 | 11 | /// Copied from Dart SDK 12 | class _SystemHash { 13 | _SystemHash._(); 14 | 15 | static int combine(int hash, int value) { 16 | // ignore: parameter_assignments 17 | hash = 0x1fffffff & (hash + value); 18 | // ignore: parameter_assignments 19 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 20 | return hash ^ (hash >> 6); 21 | } 22 | 23 | static int finish(int hash) { 24 | // ignore: parameter_assignments 25 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 26 | // ignore: parameter_assignments 27 | hash = hash ^ (hash >> 11); 28 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 29 | } 30 | } 31 | 32 | abstract class _$ActivitiesList 33 | extends BuildlessAutoDisposeAsyncNotifier> { 34 | late final String tripId; 35 | 36 | FutureOr> build( 37 | String tripId, 38 | ); 39 | } 40 | 41 | /// See also [ActivitiesList]. 42 | @ProviderFor(ActivitiesList) 43 | const activitiesListProvider = ActivitiesListFamily(); 44 | 45 | /// See also [ActivitiesList]. 46 | class ActivitiesListFamily extends Family>> { 47 | /// See also [ActivitiesList]. 48 | const ActivitiesListFamily(); 49 | 50 | /// See also [ActivitiesList]. 51 | ActivitiesListProvider call( 52 | String tripId, 53 | ) { 54 | return ActivitiesListProvider( 55 | tripId, 56 | ); 57 | } 58 | 59 | @override 60 | ActivitiesListProvider getProviderOverride( 61 | covariant ActivitiesListProvider provider, 62 | ) { 63 | return call( 64 | provider.tripId, 65 | ); 66 | } 67 | 68 | static const Iterable? _dependencies = null; 69 | 70 | @override 71 | Iterable? get dependencies => _dependencies; 72 | 73 | static const Iterable? _allTransitiveDependencies = null; 74 | 75 | @override 76 | Iterable? get allTransitiveDependencies => 77 | _allTransitiveDependencies; 78 | 79 | @override 80 | String? get name => r'activitiesListProvider'; 81 | } 82 | 83 | /// See also [ActivitiesList]. 84 | class ActivitiesListProvider extends AutoDisposeAsyncNotifierProviderImpl< 85 | ActivitiesList, List> { 86 | /// See also [ActivitiesList]. 87 | ActivitiesListProvider( 88 | this.tripId, 89 | ) : super.internal( 90 | () => ActivitiesList()..tripId = tripId, 91 | from: activitiesListProvider, 92 | name: r'activitiesListProvider', 93 | debugGetCreateSourceHash: 94 | const bool.fromEnvironment('dart.vm.product') 95 | ? null 96 | : _$activitiesListHash, 97 | dependencies: ActivitiesListFamily._dependencies, 98 | allTransitiveDependencies: 99 | ActivitiesListFamily._allTransitiveDependencies, 100 | ); 101 | 102 | final String tripId; 103 | 104 | @override 105 | bool operator ==(Object other) { 106 | return other is ActivitiesListProvider && other.tripId == tripId; 107 | } 108 | 109 | @override 110 | int get hashCode { 111 | var hash = _SystemHash.combine(0, runtimeType.hashCode); 112 | hash = _SystemHash.combine(hash, tripId.hashCode); 113 | 114 | return _SystemHash.finish(hash); 115 | } 116 | 117 | @override 118 | FutureOr> runNotifierBuild( 119 | covariant ActivitiesList notifier, 120 | ) { 121 | return notifier.build( 122 | tripId, 123 | ); 124 | } 125 | } 126 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 127 | -------------------------------------------------------------------------------- /lib/features/activity/controller/activity_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'activity_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$activityControllerHash() => 10 | r'0c2a1e12050057b93ca2df2ca0d8530d9ee6956f'; 11 | 12 | /// Copied from Dart SDK 13 | class _SystemHash { 14 | _SystemHash._(); 15 | 16 | static int combine(int hash, int value) { 17 | // ignore: parameter_assignments 18 | hash = 0x1fffffff & (hash + value); 19 | // ignore: parameter_assignments 20 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 21 | return hash ^ (hash >> 6); 22 | } 23 | 24 | static int finish(int hash) { 25 | // ignore: parameter_assignments 26 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 27 | // ignore: parameter_assignments 28 | hash = hash ^ (hash >> 11); 29 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 30 | } 31 | } 32 | 33 | abstract class _$ActivityController 34 | extends BuildlessAutoDisposeAsyncNotifier { 35 | late final String activityId; 36 | 37 | FutureOr build( 38 | String activityId, 39 | ); 40 | } 41 | 42 | /// See also [ActivityController]. 43 | @ProviderFor(ActivityController) 44 | const activityControllerProvider = ActivityControllerFamily(); 45 | 46 | /// See also [ActivityController]. 47 | class ActivityControllerFamily extends Family> { 48 | /// See also [ActivityController]. 49 | const ActivityControllerFamily(); 50 | 51 | /// See also [ActivityController]. 52 | ActivityControllerProvider call( 53 | String activityId, 54 | ) { 55 | return ActivityControllerProvider( 56 | activityId, 57 | ); 58 | } 59 | 60 | @override 61 | ActivityControllerProvider getProviderOverride( 62 | covariant ActivityControllerProvider provider, 63 | ) { 64 | return call( 65 | provider.activityId, 66 | ); 67 | } 68 | 69 | static const Iterable? _dependencies = null; 70 | 71 | @override 72 | Iterable? get dependencies => _dependencies; 73 | 74 | static const Iterable? _allTransitiveDependencies = null; 75 | 76 | @override 77 | Iterable? get allTransitiveDependencies => 78 | _allTransitiveDependencies; 79 | 80 | @override 81 | String? get name => r'activityControllerProvider'; 82 | } 83 | 84 | /// See also [ActivityController]. 85 | class ActivityControllerProvider 86 | extends AutoDisposeAsyncNotifierProviderImpl { 87 | /// See also [ActivityController]. 88 | ActivityControllerProvider( 89 | this.activityId, 90 | ) : super.internal( 91 | () => ActivityController()..activityId = activityId, 92 | from: activityControllerProvider, 93 | name: r'activityControllerProvider', 94 | debugGetCreateSourceHash: 95 | const bool.fromEnvironment('dart.vm.product') 96 | ? null 97 | : _$activityControllerHash, 98 | dependencies: ActivityControllerFamily._dependencies, 99 | allTransitiveDependencies: 100 | ActivityControllerFamily._allTransitiveDependencies, 101 | ); 102 | 103 | final String activityId; 104 | 105 | @override 106 | bool operator ==(Object other) { 107 | return other is ActivityControllerProvider && 108 | other.activityId == activityId; 109 | } 110 | 111 | @override 112 | int get hashCode { 113 | var hash = _SystemHash.combine(0, runtimeType.hashCode); 114 | hash = _SystemHash.combine(hash, activityId.hashCode); 115 | 116 | return _SystemHash.finish(hash); 117 | } 118 | 119 | @override 120 | FutureOr runNotifierBuild( 121 | covariant ActivityController notifier, 122 | ) { 123 | return notifier.build( 124 | activityId, 125 | ); 126 | } 127 | } 128 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions 129 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trips_gridview/trip_gridview_item_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 2 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 3 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 4 | import 'package:cached_network_image/cached_network_image.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class TripGridViewItemCard extends StatelessWidget { 8 | const TripGridViewItemCard({ 9 | required this.trip, 10 | super.key, 11 | }); 12 | 13 | final Trip trip; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Card( 18 | clipBehavior: Clip.antiAlias, 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(15), 21 | ), 22 | elevation: 5, 23 | child: Column( 24 | children: [ 25 | Expanded( 26 | child: Container( 27 | height: 500, 28 | alignment: Alignment.center, 29 | color: const Color(constants.primaryColorDark), 30 | child: Stack( 31 | children: [ 32 | Positioned.fill( 33 | child: trip.tripImageUrl != null 34 | ? Stack( 35 | children: [ 36 | const Center(child: CircularProgressIndicator()), 37 | CachedNetworkImage( 38 | errorWidget: (context, url, dynamic error) => 39 | const Icon(Icons.error_outline_outlined), 40 | imageUrl: trip.tripImageUrl!, 41 | cacheKey: trip.tripImageKey, 42 | width: double.maxFinite, 43 | height: 500, 44 | alignment: Alignment.topCenter, 45 | fit: BoxFit.fill, 46 | ), 47 | ], 48 | ) 49 | : Image.asset( 50 | 'images/amplify.png', 51 | fit: BoxFit.contain, 52 | ), 53 | ), 54 | Positioned( 55 | bottom: 16, 56 | left: 16, 57 | right: 16, 58 | child: FittedBox( 59 | fit: BoxFit.scaleDown, 60 | alignment: Alignment.centerLeft, 61 | child: Text( 62 | trip.destination, 63 | style: Theme.of(context) 64 | .textTheme 65 | .headlineSmall! 66 | .copyWith(color: Colors.white), 67 | ), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ), 74 | Padding( 75 | padding: const EdgeInsets.fromLTRB(2, 8, 8, 4), 76 | child: DefaultTextStyle( 77 | softWrap: false, 78 | overflow: TextOverflow.ellipsis, 79 | style: Theme.of(context).textTheme.titleMedium!, 80 | child: Column( 81 | crossAxisAlignment: CrossAxisAlignment.start, 82 | children: [ 83 | Padding( 84 | padding: const EdgeInsets.only(bottom: 8), 85 | child: Text( 86 | trip.tripName, 87 | style: Theme.of(context) 88 | .textTheme 89 | .titleMedium! 90 | .copyWith(color: Colors.black54), 91 | ), 92 | ), 93 | Text( 94 | trip.startDate.getDateTime().format('MMMM dd, yyyy'), 95 | style: const TextStyle(fontSize: 12), 96 | ), 97 | Text( 98 | trip.endDate.getDateTime().format('MMMM dd, yyyy'), 99 | style: const TextStyle(fontSize: 12), 100 | ), 101 | ], 102 | ), 103 | ), 104 | ), 105 | ], 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/features/profile/ui/profile_page/profile_listview.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 2 | import 'package:amplify_trips_planner/features/profile/controller/profile_controller.dart'; 3 | import 'package:amplify_trips_planner/features/profile/ui/profile_page/edit_profile_bottomsheet.dart'; 4 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class ProfileListView extends ConsumerWidget { 9 | const ProfileListView({ 10 | required this.profile, 11 | super.key, 12 | }); 13 | 14 | final AsyncValue profile; 15 | 16 | void editProfile(BuildContext context, Profile profile) async { 17 | await showModalBottomSheet( 18 | isScrollControlled: true, 19 | elevation: 5, 20 | context: context, 21 | builder: (BuildContext context) { 22 | return EditProfileBottomSheet( 23 | profile: profile, 24 | ); 25 | }, 26 | ); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context, WidgetRef ref) { 31 | switch (profile) { 32 | case AsyncData(:final value): 33 | return ListView( 34 | children: [ 35 | Card( 36 | child: ListTile( 37 | leading: const Icon( 38 | Icons.verified_user, 39 | size: 50, 40 | color: Color(constants.primaryColorDark), 41 | ), 42 | title: Text( 43 | value.firstName != null ? value.firstName! : 'Add your name', 44 | style: Theme.of(context).textTheme.titleLarge, 45 | ), 46 | subtitle: Text(value.email), 47 | ), 48 | ), 49 | ListTile( 50 | dense: true, 51 | title: Text( 52 | 'Home', 53 | style: Theme.of(context) 54 | .textTheme 55 | .titleSmall! 56 | .copyWith(color: Colors.white), 57 | ), 58 | tileColor: Colors.grey, 59 | ), 60 | Card( 61 | child: ListTile( 62 | title: Text( 63 | value.firstName != null ? value.homeCity! : 'Add your city', 64 | style: Theme.of(context).textTheme.titleLarge, 65 | ), 66 | ), 67 | ), 68 | const ListTile( 69 | dense: true, 70 | tileColor: Colors.grey, 71 | visualDensity: VisualDensity(vertical: -4), 72 | ), 73 | Card( 74 | child: Row( 75 | mainAxisAlignment: MainAxisAlignment.spaceAround, 76 | children: [ 77 | TextButton( 78 | style: TextButton.styleFrom( 79 | textStyle: const TextStyle(fontSize: 20), 80 | ), 81 | onPressed: () { 82 | editProfile(context, value); 83 | }, 84 | child: const Text('Edit'), 85 | ), 86 | ], 87 | ), 88 | ) 89 | ], 90 | ); 91 | 92 | case AsyncError(): 93 | return Column( 94 | mainAxisSize: MainAxisSize.min, 95 | crossAxisAlignment: CrossAxisAlignment.center, 96 | children: [ 97 | Padding( 98 | padding: const EdgeInsets.all(8), 99 | child: Text( 100 | 'Error', 101 | style: Theme.of(context).textTheme.titleMedium, 102 | textAlign: TextAlign.center, 103 | ), 104 | ), 105 | TextButton( 106 | style: TextButton.styleFrom( 107 | textStyle: const TextStyle(fontSize: 20), 108 | ), 109 | onPressed: () async { 110 | ref.invalidate(profileControllerProvider); 111 | }, 112 | child: const Text('Try again'), 113 | ), 114 | ], 115 | ); 116 | case AsyncLoading(): 117 | return const Center( 118 | child: CircularProgressIndicator(), 119 | ); 120 | 121 | case _: 122 | return const Center( 123 | child: Text('Error'), 124 | ); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trips_list/add_trip_bottomsheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart'; 2 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 3 | import 'package:amplify_trips_planner/features/trip/controller/trips_list.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | class AddTripBottomSheet extends ConsumerWidget { 9 | AddTripBottomSheet({ 10 | super.key, 11 | }); 12 | 13 | final formGlobalKey = GlobalKey(); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final tripNameController = TextEditingController(); 18 | final destinationController = TextEditingController(); 19 | final startDateController = TextEditingController(); 20 | final endDateController = TextEditingController(); 21 | 22 | return Form( 23 | key: formGlobalKey, 24 | child: Container( 25 | padding: EdgeInsets.only( 26 | top: 15, 27 | left: 15, 28 | right: 15, 29 | bottom: MediaQuery.of(context).viewInsets.bottom + 15, 30 | ), 31 | width: double.infinity, 32 | child: Column( 33 | mainAxisSize: MainAxisSize.min, 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | BottomSheetTextFormField( 37 | labelText: 'Trip Name', 38 | controller: tripNameController, 39 | keyboardType: TextInputType.name, 40 | ), 41 | const SizedBox( 42 | height: 20, 43 | ), 44 | BottomSheetTextFormField( 45 | labelText: 'Trip Destination', 46 | controller: destinationController, 47 | keyboardType: TextInputType.name, 48 | ), 49 | const SizedBox( 50 | height: 20, 51 | ), 52 | BottomSheetTextFormField( 53 | labelText: 'Start Date', 54 | controller: startDateController, 55 | keyboardType: TextInputType.datetime, 56 | onTap: () async { 57 | final pickedDate = await showDatePicker( 58 | context: context, 59 | initialDate: DateTime.now(), 60 | firstDate: DateTime(2000), 61 | lastDate: DateTime(2101), 62 | ); 63 | 64 | if (pickedDate != null) { 65 | startDateController.text = pickedDate.format('yyyy-MM-dd'); 66 | } 67 | }, 68 | ), 69 | const SizedBox( 70 | height: 20, 71 | ), 72 | BottomSheetTextFormField( 73 | labelText: 'End Date', 74 | controller: endDateController, 75 | keyboardType: TextInputType.datetime, 76 | onTap: () async { 77 | if (startDateController.text.isNotEmpty) { 78 | final pickedDate = await showDatePicker( 79 | context: context, 80 | initialDate: DateTime.parse(startDateController.text), 81 | firstDate: DateTime.parse(startDateController.text), 82 | lastDate: DateTime(2101), 83 | ); 84 | 85 | if (pickedDate != null) { 86 | endDateController.text = pickedDate.format('yyyy-MM-dd'); 87 | } 88 | } 89 | }, 90 | ), 91 | const SizedBox( 92 | height: 20, 93 | ), 94 | TextButton( 95 | child: const Text('OK'), 96 | onPressed: () async { 97 | final currentState = formGlobalKey.currentState; 98 | if (currentState == null) { 99 | return; 100 | } 101 | if (currentState.validate()) { 102 | await ref.watch(tripsListProvider.notifier).addTrip( 103 | name: tripNameController.text, 104 | destination: destinationController.text, 105 | startDate: startDateController.text, 106 | endDate: endDateController.text, 107 | ); 108 | 109 | if (context.mounted) { 110 | context.pop(); 111 | } 112 | } 113 | }, //, 114 | ), 115 | ], 116 | ), 117 | ), 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - amplify_auth_cognito_ios (0.0.1): 3 | - Flutter 4 | - amplify_secure_storage (0.0.1): 5 | - Flutter 6 | - connectivity_plus (0.0.1): 7 | - Flutter 8 | - ReachabilitySwift 9 | - device_info_plus (0.0.1): 10 | - Flutter 11 | - DKImagePickerController/Core (4.3.4): 12 | - DKImagePickerController/ImageDataManager 13 | - DKImagePickerController/Resource 14 | - DKImagePickerController/ImageDataManager (4.3.4) 15 | - DKImagePickerController/PhotoGallery (4.3.4): 16 | - DKImagePickerController/Core 17 | - DKPhotoGallery 18 | - DKImagePickerController/Resource (4.3.4) 19 | - DKPhotoGallery (0.0.17): 20 | - DKPhotoGallery/Core (= 0.0.17) 21 | - DKPhotoGallery/Model (= 0.0.17) 22 | - DKPhotoGallery/Preview (= 0.0.17) 23 | - DKPhotoGallery/Resource (= 0.0.17) 24 | - SDWebImage 25 | - SwiftyGif 26 | - DKPhotoGallery/Core (0.0.17): 27 | - DKPhotoGallery/Model 28 | - DKPhotoGallery/Preview 29 | - SDWebImage 30 | - SwiftyGif 31 | - DKPhotoGallery/Model (0.0.17): 32 | - SDWebImage 33 | - SwiftyGif 34 | - DKPhotoGallery/Preview (0.0.17): 35 | - DKPhotoGallery/Model 36 | - DKPhotoGallery/Resource 37 | - SDWebImage 38 | - SwiftyGif 39 | - DKPhotoGallery/Resource (0.0.17): 40 | - SDWebImage 41 | - SwiftyGif 42 | - file_picker (0.0.1): 43 | - DKImagePickerController/PhotoGallery 44 | - Flutter 45 | - Flutter (1.0.0) 46 | - FMDB (2.7.5): 47 | - FMDB/standard (= 2.7.5) 48 | - FMDB/standard (2.7.5) 49 | - image_picker_ios (0.0.1): 50 | - Flutter 51 | - package_info_plus (0.4.5): 52 | - Flutter 53 | - path_provider_foundation (0.0.1): 54 | - Flutter 55 | - FlutterMacOS 56 | - ReachabilitySwift (5.0.0) 57 | - SDWebImage (5.15.5): 58 | - SDWebImage/Core (= 5.15.5) 59 | - SDWebImage/Core (5.15.5) 60 | - sqflite (0.0.2): 61 | - Flutter 62 | - FMDB (>= 2.7.5) 63 | - SwiftyGif (5.4.4) 64 | - url_launcher_ios (0.0.1): 65 | - Flutter 66 | 67 | DEPENDENCIES: 68 | - amplify_auth_cognito_ios (from `.symlinks/plugins/amplify_auth_cognito_ios/ios`) 69 | - amplify_secure_storage (from `.symlinks/plugins/amplify_secure_storage/ios`) 70 | - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) 71 | - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 72 | - file_picker (from `.symlinks/plugins/file_picker/ios`) 73 | - Flutter (from `Flutter`) 74 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 75 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 76 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 77 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 78 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 79 | 80 | SPEC REPOS: 81 | trunk: 82 | - DKImagePickerController 83 | - DKPhotoGallery 84 | - FMDB 85 | - ReachabilitySwift 86 | - SDWebImage 87 | - SwiftyGif 88 | 89 | EXTERNAL SOURCES: 90 | amplify_auth_cognito_ios: 91 | :path: ".symlinks/plugins/amplify_auth_cognito_ios/ios" 92 | amplify_secure_storage: 93 | :path: ".symlinks/plugins/amplify_secure_storage/ios" 94 | connectivity_plus: 95 | :path: ".symlinks/plugins/connectivity_plus/ios" 96 | device_info_plus: 97 | :path: ".symlinks/plugins/device_info_plus/ios" 98 | file_picker: 99 | :path: ".symlinks/plugins/file_picker/ios" 100 | Flutter: 101 | :path: Flutter 102 | image_picker_ios: 103 | :path: ".symlinks/plugins/image_picker_ios/ios" 104 | package_info_plus: 105 | :path: ".symlinks/plugins/package_info_plus/ios" 106 | path_provider_foundation: 107 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 108 | sqflite: 109 | :path: ".symlinks/plugins/sqflite/ios" 110 | url_launcher_ios: 111 | :path: ".symlinks/plugins/url_launcher_ios/ios" 112 | 113 | SPEC CHECKSUMS: 114 | amplify_auth_cognito_ios: f8b7c4d978565313b60596d7dc94fe029b4f7704 115 | amplify_secure_storage: 501cbdac671a050090d48dfcc002d5637d3f4a55 116 | connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e 117 | device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed 118 | DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac 119 | DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 120 | file_picker: ce3938a0df3cc1ef404671531facef740d03f920 121 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 122 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 123 | image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb 124 | package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e 125 | path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 126 | ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 127 | SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe 128 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 129 | SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f 130 | url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993 131 | 132 | PODFILE CHECKSUM: a843a48598f7673f7f2af9f25c841666a29f3416 133 | 134 | COCOAPODS: 1.11.3 135 | -------------------------------------------------------------------------------- /lib/features/trip/ui/trip_page/selected_trip_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 4 | import 'package:amplify_trips_planner/common/ui/upload_progress_dialog.dart'; 5 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 6 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 7 | import 'package:amplify_trips_planner/features/trip/controller/trips_list.dart'; 8 | import 'package:amplify_trips_planner/features/trip/ui/trip_page/delete_trip_dialog.dart'; 9 | import 'package:amplify_trips_planner/models/Trip.dart'; 10 | import 'package:cached_network_image/cached_network_image.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 13 | import 'package:go_router/go_router.dart'; 14 | import 'package:image_picker/image_picker.dart'; 15 | 16 | class SelectedTripCard extends ConsumerWidget { 17 | const SelectedTripCard({ 18 | required this.trip, 19 | super.key, 20 | }); 21 | 22 | final Trip trip; 23 | 24 | Future uploadImage({ 25 | required BuildContext context, 26 | required WidgetRef ref, 27 | required Trip trip, 28 | }) async { 29 | final picker = ImagePicker(); 30 | final pickedFile = await picker.pickImage(source: ImageSource.gallery); 31 | if (pickedFile == null) { 32 | return false; 33 | } 34 | final file = File(pickedFile.path); 35 | if (context.mounted) { 36 | await showDialog( 37 | context: context, 38 | barrierDismissible: false, 39 | builder: (BuildContext context) { 40 | return const UploadProgressDialog(); 41 | }, 42 | ); 43 | 44 | await ref 45 | .watch(tripControllerProvider(trip.id).notifier) 46 | .uploadFile(file, trip); 47 | } 48 | 49 | return true; 50 | } 51 | 52 | Future deleteTrip( 53 | BuildContext context, 54 | WidgetRef ref, 55 | Trip trip, 56 | ) async { 57 | var value = await showDialog( 58 | context: context, 59 | builder: (BuildContext context) { 60 | return const DeleteTripDialog(); 61 | }, 62 | ); 63 | value ??= false; 64 | 65 | if (value) { 66 | await ref.watch(tripsListProvider.notifier).removeTrip(trip); 67 | } 68 | return value; 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context, WidgetRef ref) { 73 | return Card( 74 | clipBehavior: Clip.antiAlias, 75 | shape: RoundedRectangleBorder( 76 | borderRadius: BorderRadius.circular(15), 77 | ), 78 | elevation: 5, 79 | child: Column( 80 | mainAxisSize: MainAxisSize.min, 81 | children: [ 82 | Text( 83 | trip.tripName, 84 | textAlign: TextAlign.center, 85 | style: const TextStyle( 86 | fontSize: 20, 87 | fontWeight: FontWeight.bold, 88 | ), 89 | ), 90 | const SizedBox( 91 | height: 8, 92 | ), 93 | Container( 94 | alignment: Alignment.center, 95 | color: const Color(constants.primaryColorDark), //Color(0xffE1E5E4), 96 | height: 150, 97 | 98 | child: trip.tripImageUrl != null 99 | ? Stack( 100 | children: [ 101 | const Center(child: CircularProgressIndicator()), 102 | CachedNetworkImage( 103 | cacheKey: trip.tripImageKey, 104 | imageUrl: trip.tripImageUrl!, 105 | width: double.maxFinite, 106 | height: 500, 107 | alignment: Alignment.topCenter, 108 | fit: BoxFit.fill, 109 | ), 110 | ], 111 | ) 112 | : Image.asset( 113 | 'images/amplify.png', 114 | fit: BoxFit.contain, 115 | ), 116 | ), 117 | Row( 118 | mainAxisAlignment: MainAxisAlignment.spaceAround, 119 | children: [ 120 | IconButton( 121 | onPressed: () { 122 | context.goNamed( 123 | AppRoute.editTrip.name, 124 | pathParameters: {'id': trip.id}, 125 | extra: trip, 126 | ); 127 | }, 128 | icon: const Icon(Icons.edit), 129 | ), 130 | IconButton( 131 | onPressed: () { 132 | uploadImage( 133 | context: context, 134 | trip: trip, 135 | ref: ref, 136 | ).then((value) { 137 | if (value) { 138 | Navigator.of(context, rootNavigator: true).pop(); 139 | ref.invalidate(tripControllerProvider(trip.id)); 140 | } 141 | }); 142 | }, 143 | icon: const Icon(Icons.camera_enhance_sharp), 144 | ), 145 | IconButton( 146 | onPressed: () { 147 | deleteTrip(context, ref, trip).then((value) { 148 | if (value) { 149 | context.goNamed( 150 | AppRoute.home.name, 151 | ); 152 | } 153 | }); 154 | }, 155 | icon: const Icon(Icons.delete), 156 | ), 157 | ], 158 | ) 159 | ], 160 | ), 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/features/trip/ui/edit_trip_page/edit_trip_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_flutter/amplify_flutter.dart'; 2 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 3 | import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart'; 4 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 5 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 6 | import 'package:amplify_trips_planner/features/trip/controller/trip_controller.dart'; 7 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | import 'package:go_router/go_router.dart'; 11 | 12 | class EditTripPage extends ConsumerWidget { 13 | EditTripPage({ 14 | required this.trip, 15 | super.key, 16 | }); 17 | final Trip trip; 18 | 19 | final formGlobalKey = GlobalKey(); 20 | 21 | @override 22 | Widget build(BuildContext context, WidgetRef ref) { 23 | final tripNameController = TextEditingController(text: trip.tripName); 24 | final destinationController = TextEditingController(text: trip.destination); 25 | final startDateController = TextEditingController( 26 | text: trip.startDate.getDateTime().format('yyyy-MM-dd'), 27 | ); 28 | 29 | final endDateController = TextEditingController( 30 | text: trip.endDate.getDateTime().format('yyyy-MM-dd'), 31 | ); 32 | 33 | return Scaffold( 34 | appBar: AppBar( 35 | centerTitle: true, 36 | title: const Text( 37 | 'Amplify Trips Planner', 38 | ), 39 | leading: IconButton( 40 | onPressed: () { 41 | context.goNamed( 42 | AppRoute.trip.name, 43 | pathParameters: {'id': trip.id}, 44 | ); 45 | }, 46 | icon: const Icon(Icons.arrow_back), 47 | ), 48 | backgroundColor: const Color(constants.primaryColorDark), 49 | ), 50 | body: SingleChildScrollView( 51 | child: Form( 52 | key: formGlobalKey, 53 | child: Container( 54 | padding: EdgeInsets.only( 55 | top: 15, 56 | left: 15, 57 | right: 15, 58 | bottom: MediaQuery.of(context).viewInsets.bottom + 15, 59 | ), 60 | width: double.infinity, 61 | child: Column( 62 | mainAxisSize: MainAxisSize.min, 63 | crossAxisAlignment: CrossAxisAlignment.start, 64 | children: [ 65 | BottomSheetTextFormField( 66 | labelText: 'Trip Name', 67 | controller: tripNameController, 68 | keyboardType: TextInputType.name, 69 | ), 70 | const SizedBox( 71 | height: 20, 72 | ), 73 | BottomSheetTextFormField( 74 | labelText: 'Trip Destination', 75 | controller: destinationController, 76 | keyboardType: TextInputType.name, 77 | ), 78 | const SizedBox( 79 | height: 20, 80 | ), 81 | BottomSheetTextFormField( 82 | labelText: 'Start Date', 83 | controller: startDateController, 84 | keyboardType: TextInputType.datetime, 85 | onTap: () async { 86 | final pickedDate = await showDatePicker( 87 | context: context, 88 | initialDate: DateTime.now(), 89 | firstDate: DateTime(2000), 90 | lastDate: DateTime(2101), 91 | ); 92 | 93 | if (pickedDate != null) { 94 | startDateController.text = 95 | pickedDate.format('yyyy-MM-dd'); 96 | } 97 | }, 98 | ), 99 | const SizedBox( 100 | height: 20, 101 | ), 102 | BottomSheetTextFormField( 103 | labelText: 'End Date', 104 | controller: endDateController, 105 | keyboardType: TextInputType.datetime, 106 | onTap: () async { 107 | if (startDateController.text.isNotEmpty) { 108 | final pickedDate = await showDatePicker( 109 | context: context, 110 | initialDate: DateTime.parse(startDateController.text), 111 | firstDate: DateTime.parse(startDateController.text), 112 | lastDate: DateTime(2101), 113 | ); 114 | 115 | if (pickedDate != null) { 116 | endDateController.text = 117 | pickedDate.format('yyyy-MM-dd'); 118 | } 119 | } 120 | }, 121 | ), 122 | const SizedBox( 123 | height: 20, 124 | ), 125 | TextButton( 126 | child: const Text('OK'), 127 | onPressed: () async { 128 | final currentState = formGlobalKey.currentState; 129 | if (currentState == null) { 130 | return; 131 | } 132 | if (currentState.validate()) { 133 | final updatedTrip = trip.copyWith( 134 | tripName: tripNameController.text, 135 | destination: destinationController.text, 136 | startDate: TemporalDate( 137 | DateTime.parse(startDateController.text), 138 | ), 139 | endDate: TemporalDate( 140 | DateTime.parse(endDateController.text), 141 | ), 142 | ); 143 | 144 | await ref 145 | .watch(tripControllerProvider(trip.id).notifier) 146 | .updateTrip(updatedTrip); 147 | if (context.mounted) { 148 | context.goNamed( 149 | AppRoute.trip.name, 150 | pathParameters: {'id': trip.id}, 151 | extra: updatedTrip, 152 | ); 153 | } 154 | } 155 | }, //, 156 | ), 157 | ], 158 | ), 159 | ), 160 | ), 161 | ), 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/features/activity/ui/add_activity/add_activity_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 2 | import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart'; 3 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 4 | import 'package:amplify_trips_planner/features/activity/controller/activities_list.dart'; 5 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | import 'package:go_router/go_router.dart'; 9 | 10 | class AddActivityForm extends ConsumerWidget { 11 | AddActivityForm({ 12 | required this.trip, 13 | super.key, 14 | }); 15 | 16 | final AsyncValue trip; 17 | 18 | final formGlobalKey = GlobalKey(); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final activityNameController = TextEditingController(); 23 | final activityDateController = TextEditingController(); 24 | final activityTimeController = TextEditingController(); 25 | var activityCategory = ActivityCategory.Flight; 26 | var activityTime = TimeOfDay.now(); 27 | switch (trip) { 28 | case AsyncData(:final value): 29 | return SingleChildScrollView( 30 | child: Form( 31 | key: formGlobalKey, 32 | child: Container( 33 | padding: EdgeInsets.only( 34 | top: 15, 35 | left: 15, 36 | right: 15, 37 | bottom: MediaQuery.of(context).viewInsets.bottom + 15, 38 | ), 39 | width: double.infinity, 40 | child: Column( 41 | mainAxisSize: MainAxisSize.min, 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | BottomSheetTextFormField( 45 | labelText: 'Activity Name', 46 | controller: activityNameController, 47 | keyboardType: TextInputType.name, 48 | ), 49 | const SizedBox( 50 | height: 20, 51 | ), 52 | DropdownButtonFormField( 53 | onChanged: (value) { 54 | activityCategory = value!; 55 | }, 56 | value: activityCategory, 57 | decoration: const InputDecoration( 58 | labelText: 'Category', 59 | ), 60 | items: [ 61 | for (var category in ActivityCategory.values) 62 | DropdownMenuItem( 63 | value: category, 64 | child: Text(category.name), 65 | ), 66 | ], 67 | ), 68 | const SizedBox( 69 | height: 20, 70 | ), 71 | BottomSheetTextFormField( 72 | labelText: 'Activity Date', 73 | controller: activityDateController, 74 | keyboardType: TextInputType.datetime, 75 | onTap: () async { 76 | final pickedDate = await showDatePicker( 77 | context: context, 78 | initialDate: DateTime.parse(value.startDate.toString()), 79 | firstDate: DateTime.parse(value.startDate.toString()), 80 | lastDate: DateTime.parse(value.endDate.toString()), 81 | ); 82 | 83 | if (pickedDate != null) { 84 | activityDateController.text = 85 | pickedDate.format('yyyy-MM-dd'); 86 | } else {} 87 | }, 88 | ), 89 | const SizedBox( 90 | height: 20, 91 | ), 92 | BottomSheetTextFormField( 93 | labelText: 'Activity Time', 94 | controller: activityTimeController, 95 | keyboardType: TextInputType.datetime, 96 | onTap: () async { 97 | await showTimePicker( 98 | context: context, 99 | initialTime: activityTime, 100 | initialEntryMode: TimePickerEntryMode.dial, 101 | ).then((timeOfDay) { 102 | if (timeOfDay != null) { 103 | final localizations = 104 | MaterialLocalizations.of(context); 105 | final formattedTimeOfDay = 106 | localizations.formatTimeOfDay(timeOfDay); 107 | 108 | activityTimeController.text = formattedTimeOfDay; 109 | activityTime = timeOfDay; 110 | } 111 | }); 112 | }, 113 | ), 114 | const SizedBox( 115 | height: 20, 116 | ), 117 | TextButton( 118 | child: const Text('OK'), 119 | onPressed: () async { 120 | final currentState = formGlobalKey.currentState; 121 | if (currentState == null) { 122 | return; 123 | } 124 | if (currentState.validate()) { 125 | await ref 126 | .watch(activitiesListProvider(value.id).notifier) 127 | .add( 128 | name: activityNameController.text, 129 | activityDate: activityDateController.text, 130 | activityTime: activityTime, 131 | category: activityCategory, 132 | trip: value, 133 | ); 134 | if (context.mounted) { 135 | context.goNamed( 136 | AppRoute.trip.name, 137 | pathParameters: {'id': value.id}, 138 | ); 139 | } 140 | } 141 | }, //, 142 | ), 143 | ], 144 | ), 145 | ), 146 | ), 147 | ); 148 | 149 | case AsyncError(): 150 | return const Center( 151 | child: Text('Error'), 152 | ); 153 | case AsyncLoading(): 154 | return const Center( 155 | child: CircularProgressIndicator(), 156 | ); 157 | 158 | case _: 159 | return const Center( 160 | child: Text('Error'), 161 | ); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/features/activity/ui/edit_activity/edit_activity_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:amplify_flutter/amplify_flutter.dart'; 2 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 3 | import 'package:amplify_trips_planner/common/ui/bottomsheet_text_form_field.dart'; 4 | import 'package:amplify_trips_planner/common/utils/colors.dart' as constants; 5 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 6 | import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart'; 7 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | import 'package:go_router/go_router.dart'; 11 | import 'package:intl/intl.dart'; 12 | 13 | class EditActivityPage extends ConsumerWidget { 14 | EditActivityPage({ 15 | required this.activity, 16 | super.key, 17 | }); 18 | 19 | final Activity activity; 20 | 21 | final formGlobalKey = GlobalKey(); 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | final activityNameController = 26 | TextEditingController(text: activity.activityName); 27 | final activityDateController = TextEditingController( 28 | text: activity.activityDate.getDateTime().format('yyyy-MM-dd'), 29 | ); 30 | 31 | var activityTime = 32 | TimeOfDay.fromDateTime(activity.activityTime!.getDateTime()); 33 | final activityTimeController = TextEditingController( 34 | text: activity.activityTime!.getDateTime().format('hh:mm a'), 35 | ); 36 | 37 | final activityCategoryController = 38 | TextEditingController(text: activity.category.name); 39 | 40 | var activityCategory = activity.category; 41 | 42 | return Scaffold( 43 | appBar: AppBar( 44 | centerTitle: true, 45 | title: const Text( 46 | 'Amplify Trips Planner', 47 | ), 48 | leading: IconButton( 49 | onPressed: () { 50 | context.goNamed( 51 | AppRoute.activity.name, 52 | pathParameters: {'id': activity.id}, 53 | ); 54 | }, 55 | icon: const Icon(Icons.arrow_back), 56 | ), 57 | backgroundColor: const Color(constants.primaryColorDark), 58 | ), 59 | body: SingleChildScrollView( 60 | child: Form( 61 | key: formGlobalKey, 62 | child: Container( 63 | padding: EdgeInsets.only( 64 | top: 15, 65 | left: 15, 66 | right: 15, 67 | bottom: MediaQuery.of(context).viewInsets.bottom + 15, 68 | ), 69 | width: double.infinity, 70 | child: Column( 71 | mainAxisSize: MainAxisSize.min, 72 | crossAxisAlignment: CrossAxisAlignment.start, 73 | children: [ 74 | BottomSheetTextFormField( 75 | labelText: 'Activity Name', 76 | controller: activityNameController, 77 | keyboardType: TextInputType.name, 78 | ), 79 | const SizedBox( 80 | height: 20, 81 | ), 82 | DropdownButtonFormField( 83 | onChanged: (value) { 84 | activityCategoryController.text = value!.name; 85 | activityCategory = value; 86 | }, 87 | value: activityCategory, 88 | decoration: const InputDecoration( 89 | labelText: 'Category', 90 | ), 91 | items: [ 92 | for (var category in ActivityCategory.values) 93 | DropdownMenuItem( 94 | value: category, 95 | child: Text(category.name), 96 | ), 97 | ], 98 | ), 99 | const SizedBox( 100 | height: 20, 101 | ), 102 | BottomSheetTextFormField( 103 | labelText: 'Activity Date', 104 | controller: activityDateController, 105 | keyboardType: TextInputType.datetime, 106 | onTap: () async { 107 | final pickedDate = await showDatePicker( 108 | context: context, 109 | initialDate: 110 | DateTime.parse(activity.activityDate.toString()), 111 | firstDate: 112 | DateTime.parse(activity.trip.startDate.toString()), 113 | lastDate: 114 | DateTime.parse(activity.trip.endDate.toString()), 115 | ); 116 | 117 | if (pickedDate != null) { 118 | activityDateController.text = 119 | pickedDate.format('yyyy-MM-dd'); 120 | } else {} 121 | }, 122 | ), 123 | const SizedBox( 124 | height: 20, 125 | ), 126 | BottomSheetTextFormField( 127 | labelText: 'Activity Time', 128 | controller: activityTimeController, 129 | keyboardType: TextInputType.datetime, 130 | onTap: () async { 131 | await showTimePicker( 132 | context: context, 133 | initialTime: activityTime, 134 | initialEntryMode: TimePickerEntryMode.dial, 135 | ).then((timeOfDay) { 136 | if (timeOfDay != null) { 137 | final localizations = MaterialLocalizations.of(context); 138 | final formattedTimeOfDay = 139 | localizations.formatTimeOfDay(timeOfDay); 140 | 141 | activityTimeController.text = formattedTimeOfDay; 142 | activityTime = timeOfDay; 143 | } 144 | }); 145 | }, 146 | ), 147 | const SizedBox( 148 | height: 20, 149 | ), 150 | TextButton( 151 | child: const Text('OK'), 152 | onPressed: () async { 153 | final currentState = formGlobalKey.currentState; 154 | if (currentState == null) { 155 | return; 156 | } 157 | if (currentState.validate()) { 158 | final format = DateFormat.jm(); 159 | 160 | activityTime = TimeOfDay.fromDateTime( 161 | format.parse(activityTimeController.text), 162 | ); 163 | 164 | final now = DateTime.now(); 165 | final time = DateTime( 166 | now.year, 167 | now.month, 168 | now.day, 169 | activityTime.hour, 170 | activityTime.minute, 171 | ); 172 | 173 | final updatedActivity = activity.copyWith( 174 | category: ActivityCategory.values 175 | .byName(activityCategoryController.text), 176 | activityName: activityNameController.text, 177 | activityDate: TemporalDate( 178 | DateTime.parse(activityDateController.text), 179 | ), 180 | activityTime: TemporalTime.fromString( 181 | time.format('HH:mm:ss.sss'), 182 | ), 183 | ); 184 | 185 | await ref 186 | .watch( 187 | activityControllerProvider(activity.id).notifier, 188 | ) 189 | .updateActivity(updatedActivity); 190 | if (context.mounted) { 191 | context.goNamed( 192 | AppRoute.activity.name, 193 | pathParameters: {'id': activity.id}, 194 | ); 195 | } 196 | } 197 | }, //, 198 | ), 199 | ], 200 | ), 201 | ), 202 | ), 203 | ), 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/features/activity/ui/activity_page/activity_listview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:amplify_trips_planner/common/navigation/router/routes.dart'; 4 | import 'package:amplify_trips_planner/common/ui/upload_progress_dialog.dart'; 5 | import 'package:amplify_trips_planner/common/utils/date_time_formatter.dart'; 6 | import 'package:amplify_trips_planner/features/activity/controller/activities_list.dart'; 7 | import 'package:amplify_trips_planner/features/activity/controller/activity_controller.dart'; 8 | import 'package:amplify_trips_planner/features/activity/ui/activity_category_icon.dart'; 9 | import 'package:amplify_trips_planner/features/activity/ui/activity_page/delete_activity_dialog.dart'; 10 | import 'package:amplify_trips_planner/models/ModelProvider.dart'; 11 | import 'package:file_picker/file_picker.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 14 | import 'package:go_router/go_router.dart'; 15 | import 'package:url_launcher/url_launcher.dart'; 16 | 17 | class ActivityListView extends ConsumerWidget { 18 | const ActivityListView({ 19 | required this.activity, 20 | super.key, 21 | }); 22 | 23 | final AsyncValue activity; 24 | 25 | Future deleteActivity( 26 | BuildContext context, 27 | WidgetRef ref, 28 | Activity activity, 29 | ) async { 30 | var value = await showDialog( 31 | context: context, 32 | builder: (BuildContext context) { 33 | return const DeleteActivityDialog(); 34 | }, 35 | ); 36 | value ??= false; 37 | 38 | if (value) { 39 | await ref 40 | .watch(activitiesListProvider(activity.trip.id).notifier) 41 | .removeActivity(activity); 42 | } 43 | 44 | return value; 45 | } 46 | 47 | Future openFile({ 48 | required BuildContext context, 49 | required WidgetRef ref, 50 | required Activity activity, 51 | }) async { 52 | final fileUrl = await ref 53 | .watch(activityControllerProvider(activity.id).notifier) 54 | .getFileUrl(activity); 55 | 56 | final url = Uri.parse(fileUrl); 57 | await launchUrl(url); 58 | } 59 | 60 | Future uploadFile({ 61 | required BuildContext context, 62 | required WidgetRef ref, 63 | required Activity activity, 64 | }) async { 65 | final result = await FilePicker.platform.pickFiles( 66 | type: FileType.custom, 67 | allowedExtensions: ['jpg', 'pdf', 'png'], 68 | ); 69 | 70 | if (result == null) { 71 | return false; 72 | } 73 | 74 | final platformFile = result.files.first; 75 | 76 | final file = File(platformFile.path!); 77 | if (context.mounted) { 78 | await showDialog( 79 | context: context, 80 | barrierDismissible: false, 81 | builder: (BuildContext context) { 82 | return const UploadProgressDialog(); 83 | }, 84 | ); 85 | await ref 86 | .watch(activityControllerProvider(activity.id).notifier) 87 | .uploadFile(file, activity); 88 | } 89 | return true; 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context, WidgetRef ref) { 94 | switch (activity) { 95 | case AsyncData(:final value): 96 | return ListView( 97 | children: [ 98 | Card( 99 | child: ListTile( 100 | leading: ActivityCategoryIcon(activityCategory: value.category), 101 | title: Text( 102 | value.activityName, 103 | style: Theme.of(context).textTheme.titleLarge, 104 | ), 105 | subtitle: Text(value.category.name), 106 | ), 107 | ), 108 | ListTile( 109 | dense: true, 110 | title: Text( 111 | 'Activity Date', 112 | style: Theme.of(context) 113 | .textTheme 114 | .titleSmall! 115 | .copyWith(color: Colors.white), 116 | ), 117 | tileColor: Colors.grey, 118 | ), 119 | Card( 120 | child: ListTile( 121 | title: Text( 122 | value.activityDate.getDateTime().format('EE MMMM dd'), 123 | style: Theme.of(context).textTheme.titleLarge, 124 | ), 125 | subtitle: Text( 126 | value.activityTime!.getDateTime().format('hh:mm a'), 127 | ), 128 | ), 129 | ), 130 | ListTile( 131 | dense: true, 132 | title: Text( 133 | 'Documents', 134 | style: Theme.of(context) 135 | .textTheme 136 | .titleSmall! 137 | .copyWith(color: Colors.white), 138 | ), 139 | tileColor: Colors.grey, 140 | ), 141 | Card( 142 | child: value.activityImageUrl != null 143 | ? Row( 144 | mainAxisAlignment: MainAxisAlignment.spaceAround, 145 | children: [ 146 | TextButton( 147 | style: TextButton.styleFrom( 148 | textStyle: const TextStyle(fontSize: 20), 149 | ), 150 | onPressed: () { 151 | openFile( 152 | context: context, 153 | ref: ref, 154 | activity: value, 155 | ); 156 | }, 157 | child: const Text('Open'), 158 | ), 159 | TextButton( 160 | style: TextButton.styleFrom( 161 | textStyle: const TextStyle(fontSize: 20), 162 | ), 163 | onPressed: () { 164 | uploadFile( 165 | context: context, 166 | activity: value, 167 | ref: ref, 168 | ).then( 169 | (isUploaded) => isUploaded ? context.pop() : null, 170 | ); 171 | }, 172 | child: const Text('Replace'), 173 | ), 174 | ], 175 | ) 176 | : ListTile( 177 | title: TextButton( 178 | style: TextButton.styleFrom( 179 | textStyle: const TextStyle(fontSize: 20), 180 | ), 181 | onPressed: () { 182 | uploadFile( 183 | context: context, 184 | activity: value, 185 | ref: ref, 186 | ).then( 187 | (isUploaded) => isUploaded ? context.pop() : null, 188 | ); 189 | // Navigator.of(context, rootNavigator: true) 190 | // .pop()); 191 | }, 192 | child: const Text('Attach a PDF or photo'), 193 | ), 194 | ), 195 | ), 196 | const ListTile( 197 | dense: true, 198 | tileColor: Colors.grey, 199 | visualDensity: VisualDensity(vertical: -4), 200 | ), 201 | Card( 202 | child: Row( 203 | mainAxisAlignment: MainAxisAlignment.spaceAround, 204 | children: [ 205 | TextButton( 206 | style: TextButton.styleFrom( 207 | textStyle: const TextStyle(fontSize: 20), 208 | ), 209 | onPressed: () { 210 | context.goNamed( 211 | AppRoute.editActivity.name, 212 | pathParameters: {'id': value.id}, 213 | extra: activity, 214 | ); 215 | }, 216 | child: const Text('Edit'), 217 | ), 218 | TextButton( 219 | style: TextButton.styleFrom( 220 | textStyle: const TextStyle(fontSize: 20), 221 | ), 222 | onPressed: () { 223 | deleteActivity(context, ref, value).then( 224 | (isDeleted) { 225 | if (isDeleted) { 226 | context.goNamed( 227 | AppRoute.trip.name, 228 | pathParameters: {'id': value.trip.id}, 229 | ); 230 | } 231 | }, 232 | ); 233 | }, 234 | child: const Text('Delete'), 235 | ), 236 | ], 237 | ), 238 | ) 239 | ], 240 | ); 241 | 242 | case AsyncError(): 243 | return const Center( 244 | child: Text('Error'), 245 | ); 246 | case AsyncLoading(): 247 | return const Center( 248 | child: CircularProgressIndicator(), 249 | ); 250 | 251 | case _: 252 | return const Center( 253 | child: Text('Error'), 254 | ); 255 | } 256 | } 257 | } 258 | --------------------------------------------------------------------------------