diff --git a/Uplift.xcodeproj/project.pbxproj b/Uplift.xcodeproj/project.pbxproj index 853451a..d3faeb1 100644 --- a/Uplift.xcodeproj/project.pbxproj +++ b/Uplift.xcodeproj/project.pbxproj @@ -7,12 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 29055F802E7E3E6E00E9A730 /* UnsavedChangesModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29055F7F2E7E3E6500E9A730 /* UnsavedChangesModal.swift */; }; + 29055FC62E849E2A00E9A730 /* LoadingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29055FC52E849E2600E9A730 /* LoadingModifier.swift */; }; 291433852D87387B00F913D5 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291433842D87387500F913D5 /* UserProfile.swift */; }; 291433872D87388C00F913D5 /* WeeklyWorkoutData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291433862D87388800F913D5 /* WeeklyWorkoutData.swift */; }; 291433892D8738A400F913D5 /* WorkoutHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291433882D8738A200F913D5 /* WorkoutHistory.swift */; }; 293032572D7A8F64002E5484 /* WorkoutProgressArc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 293032562D7A8F64002E5484 /* WorkoutProgressArc.swift */; }; 294CC21D2D7E34D300EF6487 /* WeeklyWorkoutTrackerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294CC21C2D7E34D000EF6487 /* WeeklyWorkoutTrackerView.swift */; }; 296A2DAE2D7A805300EF042F /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296A2DAD2D7A805300EF042F /* ProfileViewModel.swift */; }; + 298EBA4A2E8DD244000441BA /* ModalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298EBA492E8DD241000441BA /* ModalModifier.swift */; }; + 298EBA4E2E8E504A000441BA /* GymIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298EBA4D2E8E5048000441BA /* GymIdentifier.swift */; }; + 298EBA502E8E5335000441BA /* CustomLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298EBA4F2E8E532C000441BA /* CustomLoadingView.swift */; }; + 29DC9ABA2DB96C3C0088DC24 /* CapacityReminderMutations.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 29DC9AB92DB96C340088DC24 /* CapacityReminderMutations.graphql */; }; 2CA97C8F2D8B852700EF48B3 /* UpliftAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2CA97C8E2D8B852700EF48B3 /* UpliftAPI */; }; 2E090EC52B12EF2600BAE982 /* Publishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E090EC42B12EF2600BAE982 /* Publishers.swift */; }; 2E090ECB2B12FF5900BAE982 /* UpliftAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2E090ECA2B12FF5900BAE982 /* UpliftAPI */; }; @@ -104,14 +110,14 @@ 89599A502BD4B4B600DA44DE /* FitnessClassInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89599A4F2BD4B4B600DA44DE /* FitnessClassInstance.swift */; }; 896500DC2BB4D33500D822AB /* ClassDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896500DB2BB4D33500D822AB /* ClassDetailView.swift */; }; 897703662BA2028D00F9992F /* ClassesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897703652BA2028D00F9992F /* ClassesViewModel.swift */; }; - 89C10D172CCB2F9E007E753F /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 89C10D162CCB2F9E007E753F /* FirebaseMessaging */; }; - 897DF9BA2CCDC49B00246B0D /* UpliftAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 897DF9B92CCDC49B00246B0D /* UpliftAPI */; }; + 897DF9BA2CCDC49B00246B0D /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 8983A9A62CFEA077008E84DB /* UpliftAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 8983A9A52CFEA077008E84DB /* UpliftAPI */; }; 89950D8A2B992E8400DFB007 /* ClassesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89950D892B992E8400DFB007 /* ClassesView.swift */; }; 8996FEE02BDF351800F13C67 /* NextSessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8996FEDF2BDF351800F13C67 /* NextSessionCell.swift */; }; 899B186D2CA5FAFB00FAC061 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 899B186C2CA5FAFB00FAC061 /* ProfileView.swift */; }; 89A652F92D02B00000277A16 /* CapacityRemindersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A652F62D02B00000277A16 /* CapacityRemindersView.swift */; }; 89A652FA2D02B00000277A16 /* RemindersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A652F72D02B00000277A16 /* RemindersView.swift */; }; + 89C10D172CCB2F9E007E753F /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 89C10D162CCB2F9E007E753F /* FirebaseMessaging */; }; 89C34AA12CA66E9C00C579A5 /* CapacityRemindersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89C34AA02CA66E9000C579A5 /* CapacityRemindersViewModel.swift */; }; 89C8658D2BB4779C00758337 /* ClassCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89C8658C2BB4779C00758337 /* ClassCell.swift */; }; 89CE47422D03F3C700BCB79D /* UpliftAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 89CE47412D03F3C700BCB79D /* UpliftAPI */; }; @@ -130,12 +136,18 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 29055F7F2E7E3E6500E9A730 /* UnsavedChangesModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsavedChangesModal.swift; sourceTree = ""; }; + 29055FC52E849E2600E9A730 /* LoadingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingModifier.swift; sourceTree = ""; }; 291433842D87387500F913D5 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 291433862D87388800F913D5 /* WeeklyWorkoutData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWorkoutData.swift; sourceTree = ""; }; 291433882D8738A200F913D5 /* WorkoutHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHistory.swift; sourceTree = ""; }; 293032562D7A8F64002E5484 /* WorkoutProgressArc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutProgressArc.swift; sourceTree = ""; }; 294CC21C2D7E34D000EF6487 /* WeeklyWorkoutTrackerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWorkoutTrackerView.swift; sourceTree = ""; }; 296A2DAD2D7A805300EF042F /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + 298EBA492E8DD241000441BA /* ModalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalModifier.swift; sourceTree = ""; }; + 298EBA4D2E8E5048000441BA /* GymIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymIdentifier.swift; sourceTree = ""; }; + 298EBA4F2E8E532C000441BA /* CustomLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLoadingView.swift; sourceTree = ""; }; + 29DC9AB92DB96C340088DC24 /* CapacityReminderMutations.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = CapacityReminderMutations.graphql; sourceTree = ""; }; 2E090EC42B12EF2600BAE982 /* Publishers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publishers.swift; sourceTree = ""; }; 2E090ED52B13121600BAE982 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 2E1105BE2B13B0E100119F5B /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -214,10 +226,10 @@ 897703652BA2028D00F9992F /* ClassesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassesViewModel.swift; sourceTree = ""; }; 89950D892B992E8400DFB007 /* ClassesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassesView.swift; sourceTree = ""; }; 8996FEDF2BDF351800F13C67 /* NextSessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextSessionCell.swift; sourceTree = ""; }; - 89C10D222CCB459F007E753F /* Uplift.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Uplift.entitlements; sourceTree = ""; }; 899B186C2CA5FAFB00FAC061 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 89A652F62D02B00000277A16 /* CapacityRemindersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacityRemindersView.swift; sourceTree = ""; }; 89A652F72D02B00000277A16 /* RemindersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersView.swift; sourceTree = ""; }; + 89C10D222CCB459F007E753F /* Uplift.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Uplift.entitlements; sourceTree = ""; }; 89C34AA02CA66E9000C579A5 /* CapacityRemindersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacityRemindersViewModel.swift; sourceTree = ""; }; 89C8658C2BB4779C00758337 /* ClassCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassCell.swift; sourceTree = ""; }; 89E4FAA92CEFEC3000A952B1 /* CapacitySemiCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacitySemiCircleView.swift; sourceTree = ""; }; @@ -249,7 +261,7 @@ 6338EC5F2CE14B54000BBFD3 /* UpliftAPI in Frameworks */, 2E39D82F2B3BCBA400AD238B /* UpliftAPI in Frameworks */, 6338EC5C2CE149AA000BBFD3 /* UpliftAPI in Frameworks */, - 897DF9BA2CCDC49B00246B0D /* UpliftAPI in Frameworks */, + 897DF9BA2CCDC49B00246B0D /* (null) in Frameworks */, 2E2748D22BCD4EC00023882E /* UpliftAPI in Frameworks */, 63001ADA2CC9AD980082AFFA /* GoogleSignInSwift in Frameworks */, 63A7ABCD2B86B971008D58FB /* UpliftAPI in Frameworks */, @@ -276,6 +288,7 @@ 2E090EC62B12FB9F00BAE982 /* Models */ = { isa = PBXGroup; children = ( + 298EBA4D2E8E5048000441BA /* GymIdentifier.swift */, 291433882D8738A200F913D5 /* WorkoutHistory.swift */, 291433862D87388800F913D5 /* WeeklyWorkoutData.swift */, 291433842D87387500F913D5 /* UserProfile.swift */, @@ -369,6 +382,7 @@ 2E090ED22B13093400BAE982 /* Networking */ = { isa = PBXGroup; children = ( + 29DC9AB92DB96C340088DC24 /* CapacityReminderMutations.graphql */, 2EE5F3C72B12E094008E0299 /* ApolloClientProtocol.swift */, 2EF1A2572B129EEB007A299F /* Network.swift */, 2E090EC42B12EF2600BAE982 /* Publishers.swift */, @@ -422,6 +436,7 @@ 2E15F50A2B3A0AF700414BEC /* Supporting */ = { isa = PBXGroup; children = ( + 29055F7F2E7E3E6500E9A730 /* UnsavedChangesModal.swift */, 2E15F50B2B3A0B2100414BEC /* CapacityCircleView.swift */, 89E4FAA92CEFEC3000A952B1 /* CapacitySemiCircleView.swift */, 893AD9122D59466F00C0817B /* DropDownArrow.swift */, @@ -438,6 +453,9 @@ 2E39D8192B3B5EFB00AD238B /* Custom */ = { isa = PBXGroup; children = ( + 298EBA4F2E8E532C000441BA /* CustomLoadingView.swift */, + 298EBA492E8DD241000441BA /* ModalModifier.swift */, + 29055FC52E849E2600E9A730 /* LoadingModifier.swift */, 2E6785BF2B3A5CA300DD3ADA /* Haptics.swift */, 2E39D8152B3B3AD600AD238B /* ScaleButtonStyle.swift */, 2E6785BD2B3A513000DD3ADA /* SemiCircleShape.swift */, @@ -616,10 +634,8 @@ 636E3D432BBE1F3800B6EFFC /* UpliftAPI */, 2E2748D12BCD4EC00023882E /* UpliftAPI */, 89C10D162CCB2F9E007E753F /* FirebaseMessaging */, - 897DF9B92CCDC49B00246B0D /* UpliftAPI */, 63001AD72CC9AD970082AFFA /* GoogleSignIn */, 63001AD92CC9AD970082AFFA /* GoogleSignInSwift */, - 897DF9B92CCDC49B00246B0D /* UpliftAPI */, 63001AD72CC9AD970082AFFA /* GoogleSignIn */, 63001AD92CC9AD970082AFFA /* GoogleSignInSwift */, 6338EC5B2CE149AA000BBFD3 /* UpliftAPI */, @@ -700,6 +716,7 @@ 2EB090B92B131CA80039EB3B /* Montserrat-SemiBold.ttf in Resources */, 2EB090B42B131CA40039EB3B /* BebasNeue-Regular.ttf in Resources */, 2E45B2422B4E5CE100FB83B7 /* GoogleService-Info.plist in Resources */, + 29DC9ABA2DB96C3C0088DC24 /* CapacityReminderMutations.graphql in Resources */, 2E39D8312B3BEE5900AD238B /* LaunchScreen.storyboard in Resources */, 2E8FE3962B1278B900B3DC6A /* Assets.xcassets in Resources */, ); @@ -775,10 +792,12 @@ 2E6785C42B3A780600DD3ADA /* DayOfWeek.swift in Sources */, 2E6785BE2B3A513000DD3ADA /* SemiCircleShape.swift in Sources */, 2E15F4FB2B39573C00414BEC /* Double+Extension.swift in Sources */, + 29055F802E7E3E6E00E9A730 /* UnsavedChangesModal.swift in Sources */, 2E39D8162B3B3AD600AD238B /* ScaleButtonStyle.swift in Sources */, 2E6785B92B3A425E00DD3ADA /* GymDetailView.swift in Sources */, 893AD9152D59569600C0817B /* MuscleCategoryView.swift in Sources */, 89C8658D2BB4779C00758337 /* ClassCell.swift in Sources */, + 298EBA502E8E5335000441BA /* CustomLoadingView.swift in Sources */, 291433852D87387B00F913D5 /* UserProfile.swift in Sources */, 294CC21D2D7E34D300EF6487 /* WeeklyWorkoutTrackerView.swift in Sources */, 2E1105BF2B13B0E100119F5B /* HomeViewModel.swift in Sources */, @@ -795,6 +814,7 @@ 2E39D8222B3B631200AD238B /* DividerLine.swift in Sources */, 2E15F5062B39F81B00414BEC /* MainView.swift in Sources */, 893AD9132D59468000C0817B /* DropDownArrow.swift in Sources */, + 298EBA4E2E8E504A000441BA /* GymIdentifier.swift in Sources */, 89E4FAAC2CF28E7500A952B1 /* HourlyAverageCapacity.swift in Sources */, 8996FEE02BDF351800F13C67 /* NextSessionCell.swift in Sources */, 6329A4482CA0F89D00D30A2F /* CreateProfileView.swift in Sources */, @@ -812,6 +832,7 @@ 2E15F4E62B391E5300414BEC /* Array+Extension.swift in Sources */, 2E45B2472B4F643500FB83B7 /* AnalyticsManager.swift in Sources */, 63A7ABCF2B8C119A008D58FB /* Equipment.swift in Sources */, + 29055FC62E849E2A00E9A730 /* LoadingModifier.swift in Sources */, 2E39D8202B3B623B00AD238B /* FitnessCenterView.swift in Sources */, 897703662BA2028D00F9992F /* ClassesViewModel.swift in Sources */, 2E1105C22B13D15100119F5B /* HomeGymCell.swift in Sources */, @@ -828,6 +849,7 @@ 2E15F5042B39E54700414BEC /* LocationManager.swift in Sources */, 89A652F92D02B00000277A16 /* CapacityRemindersView.swift in Sources */, 89A652FA2D02B00000277A16 /* RemindersView.swift in Sources */, + 298EBA4A2E8DD244000441BA /* ModalModifier.swift in Sources */, 2E39D81E2B3B610200AD238B /* UINavigationController+Extension.swift in Sources */, 291433892D8738A400F913D5 /* WorkoutHistory.swift in Sources */, 63001AD62CC9ACFF0082AFFA /* LoginViewModel.swift in Sources */, @@ -1273,14 +1295,14 @@ isa = XCSwiftPackageProductDependency; productName = UpliftAPI; }; - 89C10D162CCB2F9E007E753F /* FirebaseMessaging */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseMessaging; - }; 8983A9A52CFEA077008E84DB /* UpliftAPI */ = { isa = XCSwiftPackageProductDependency; productName = UpliftAPI; }; + 89C10D162CCB2F9E007E753F /* FirebaseMessaging */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseMessaging; + }; 89CE47412D03F3C700BCB79D /* UpliftAPI */ = { isa = XCSwiftPackageProductDependency; productName = UpliftAPI; diff --git a/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2edf497..2b0bca3 100644 --- a/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Uplift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "3555fb9a75a85f7fce46f4fe8623f752320bd6d5f148a5ec8078624be399326a", + "originHash" : "b1ad89ad5c696a16a25504627863e89355fc7824ecef3a5b480d13898c398b81", "pins" : [ { "identity" : "abseil-cpp-binary", "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "7ce7be095bc3ed3c98b009532fe2d7698c132614", - "version" : "1.2024011601.0" + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apollographql/apollo-ios.git", "state" : { - "revision" : "eedde2151859011a44bb7cb05388deb2bf532644", - "version" : "1.9.3" + "revision" : "51e535dcf5439c01396d668a9598748ea86c7c1a", + "version" : "1.20.0" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "c218c2054299b15ae577e818bbba16084d3eabe6", - "version" : "10.18.2" + "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d", + "version" : "10.19.2" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "888f0b6026e2441a69e3ee2ad5293c7a92031e62", - "version" : "10.23.1" + "revision" : "eca84fd638116dd6adb633b5a3f31cc7befcbb7d", + "version" : "10.29.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "c7a5917ebe48d69f421aadf154ef3969c8b7f12d", - "version" : "10.23.1" + "revision" : "fe727587518729046fc1465625b9afd80b5ab361", + "version" : "10.28.0" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", - "version" : "7.13.1" + "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", + "version" : "7.13.3" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "67043f6389d0e28b38fa02d1c6952afeb04d807f", - "version" : "1.62.1" + "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", + "version" : "1.62.2" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "5b92f029fab2cce44386d28588098b5be0824ef5", - "version" : "7.11.0" + "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", + "version" : "7.12.0" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { - "revision" : "43aaef65e0c665daadf848761d560e446d350d3d", - "version" : "1.22.4" + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "e78ae0220e17525a15ac68c697a155eb7a672a8e", - "version" : "0.15.0" + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", - "version" : "1.26.0" + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" } }, { diff --git a/Uplift/Info.plist b/Uplift/Info.plist index 0b16ae3..93b96bf 100644 --- a/Uplift/Info.plist +++ b/Uplift/Info.plist @@ -12,8 +12,6 @@ $(ANNOUNCEMENTS_SCHEME) CFBundleIconName AppIcon - FirebaseAppDelegateProxyEnabled - CFBundleURLTypes @@ -27,6 +25,8 @@ + FirebaseAppDelegateProxyEnabled + ITSAppUsesNonExemptEncryption UIAppFonts diff --git a/Uplift/Models/GymIdentifier.swift b/Uplift/Models/GymIdentifier.swift new file mode 100644 index 0000000..f145ddc --- /dev/null +++ b/Uplift/Models/GymIdentifier.swift @@ -0,0 +1,36 @@ +// +// GymIdentifier.swift +// Uplift +// +// Created by jiwon jeong on 10/2/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import Foundation + +/// An enumeration representing the gyms. +enum GymIdentifier: String, CaseIterable, Identifiable { + case teagleUp = "TEAGLEUP" + case teagleDown = "TEAGLEDOWN" + case helenNewman = "HELENNEWMAN" + case toniMorrison = "TONIMORRISON" + case noyes = "NOYES" + + var id: String { rawValue } + +} + +extension GymIdentifier { + + /// Returns the display name (capitalized) for UI. + func displayName() -> String { + switch self { + case .teagleUp: return "Teagle Up" + case .teagleDown: return "Teagle Down" + case .helenNewman: return "Helen Newman" + case .toniMorrison: return "Toni Morrison" + case .noyes: return "Noyes" + } + } + +} diff --git a/Uplift/Resources/Assets.xcassets/cross_thin.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/Contents.json new file mode 100644 index 0000000..a405583 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "cross_thin1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cross_thin2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cross_thin3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin1x.png b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin1x.png new file mode 100644 index 0000000..d5a7462 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin1x.png differ diff --git a/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin2x.png b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin2x.png new file mode 100644 index 0000000..5b90cc1 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin2x.png differ diff --git a/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin3x.png b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin3x.png new file mode 100644 index 0000000..274a02f Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/cross_thin.imageset/cross_thin3x.png differ diff --git a/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Contents.json new file mode 100644 index 0000000..1fcf0b8 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Main.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Main 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Main 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 1.png b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 1.png new file mode 100644 index 0000000..5c5acd1 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 1.png differ diff --git a/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 2.png b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 2.png new file mode 100644 index 0000000..3eb539f Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main 2.png differ diff --git a/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main.png b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main.png new file mode 100644 index 0000000..91de16f Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/logo_transparent.imageset/Main.png differ diff --git a/Uplift/Resources/Assets.xcassets/pencil.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/pencil.imageset/Contents.json new file mode 100644 index 0000000..d053487 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/pencil.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Edit.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Edit-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Edit-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-1.png b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-1.png new file mode 100644 index 0000000..ea8f171 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-1.png differ diff --git a/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-2.png b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-2.png new file mode 100644 index 0000000..14322d3 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit-2.png differ diff --git a/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit.svg b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit.svg new file mode 100644 index 0000000..2faac86 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/pencil.imageset/Edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/Uplift/Resources/Assets.xcassets/spokes.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/spokes.imageset/Contents.json new file mode 100644 index 0000000..57f4631 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/spokes.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "spokes.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "spokes (1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "spokes (2).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (1).png b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (1).png new file mode 100644 index 0000000..488a135 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (1).png differ diff --git a/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (2).png b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (2).png new file mode 100644 index 0000000..2c51346 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes (2).png differ diff --git a/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes.png b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes.png new file mode 100644 index 0000000..7a078f4 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/spokes.imageset/spokes.png differ diff --git a/Uplift/Services/Networking/CapacityReminderMutations.graphql b/Uplift/Services/Networking/CapacityReminderMutations.graphql new file mode 100644 index 0000000..1e86cd5 --- /dev/null +++ b/Uplift/Services/Networking/CapacityReminderMutations.graphql @@ -0,0 +1,41 @@ +mutation CreateCapacityReminder( + $capacityPercent: Int!, + $daysOfWeek: [String]!, + $fcmToken: String!, + $gyms: [String]! +) { + createCapacityReminder( + capacityPercent: $capacityPercent, + daysOfWeek: $daysOfWeek, + fcmToken: $fcmToken, + gyms: $gyms + ) { + id + } +} + +mutation EditCapacityReminder( + $capacityPercent: Int!, + $daysOfWeek: [String]!, + $gyms: [String]!, + $reminderId: Int! +) { + editCapacityReminder( + capacityPercent: $capacityPercent, + daysOfWeek: $daysOfWeek, + gyms: $gyms, + reminderId: $reminderId + ) { + id + } +} + +mutation DeleteCapacityReminder( + $reminderId: Int! +) { + deleteCapacityReminder( + reminderId: $reminderId + ) { + id + } +} diff --git a/Uplift/Utils/Constants.swift b/Uplift/Utils/Constants.swift index 0c93094..26f086b 100644 --- a/Uplift/Utils/Constants.swift +++ b/Uplift/Utils/Constants.swift @@ -114,12 +114,14 @@ struct Constants { static let capacity = Image("capacity") static let chest = Image("chest") static let clock = Image("clock") + static let crossThin = Image("cross_thin") static let cross = Image("cross") static let dumbbellSolid = Image("dumbbell_solid") static let dumbbellLarge = Image("dumbbell_large") static let dumbbellOutline = Image("dumbbell_outline") static let profileOutline = Image("profile_outline") static let profileSolid = Image("profile_solid") + static let pencil = Image("pencil") static let elevator = Image("elevator") static let greenTea = Image("green_tea") static let giveawayModalBackground = Image("giveaway_modal_bg") @@ -130,6 +132,7 @@ struct Constants { static let history = Image("history") static let lift = Image("lift") static let lock = Image("lock") + static let logoTransparent = Image("logo_transparent") static let logo = Image("logo") static let logoWhite = Image("logo_white") static let parking = Image("parking") @@ -137,6 +140,7 @@ struct Constants { static let replay = Image("replay") static let shoulder = Image("shoulder") static let shower = Image("shower") + static let spokes = Image("spokes") static let vertEllipsis = Image("vert_ellipsis") static let whistleOutline = Image("whistle_outline") static let whistleSolid = Image("whistle_solid") @@ -185,4 +189,12 @@ struct Constants { ) } + /// UserDefaults used in Uplift + enum UserDefaultsKeys { + static let reminderId = "savedReminderId" + static let selectedDays = "selectedDays" + static let selectedLocations = "selectedLocations" + static let capacityThreshold = "capacityThreshold" + } + } diff --git a/Uplift/Utils/Custom/CustomLoadingView.swift b/Uplift/Utils/Custom/CustomLoadingView.swift new file mode 100644 index 0000000..fb63bba --- /dev/null +++ b/Uplift/Utils/Custom/CustomLoadingView.swift @@ -0,0 +1,109 @@ +// +// CustomLoadingView.swift +// Uplift +// +// Created by jiwon jeong on 10/2/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +struct CustomLoadingView: View { + @State private var isAnimating = false + @State private var rotation: Double = 0 + + var body: some View { + ZStack { + pulsingBackground + + VStack(spacing: 30) { + spinningLogoView + loadingText + } + } + .onAppear { + isAnimating = true + withAnimation( + Animation.linear(duration: 2.0) + .repeatForever(autoreverses: false) + ) { + rotation = 360 + } + } + } + + private var loadingView: some View { + ZStack { + pulsingBackground + + VStack(spacing: 30) { + spinningLogoView + loadingText + } + } + .onAppear { + isAnimating = true + withAnimation( + Animation.linear(duration: 2.5) + .repeatForever(autoreverses: false) + ) { + rotation = 360 + } + } + } + + private var pulsingBackground: some View { + RoundedRectangle(cornerRadius: 20) + .fill(Constants.Colors.black) + .frame(width: 249, height: 271) + .scaleEffect(isAnimating ? 1.05 : 1.0) + .animation( + Animation.easeInOut(duration: 0.5) + .repeatForever(autoreverses: true), + value: isAnimating + ) + } + + private var spinningLogoView: some View { + ZStack { + spinningSpinner + bobbingLogo + } + } + + private var spinningSpinner: some View { + Constants.Images.spokes + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 175, height: 175) + .rotationEffect(.degrees(rotation)) + } + + private var bobbingLogo: some View { + Constants.Images.logoTransparent + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 176) + .shadow(color: .orange.opacity(0.5), radius: 20) + .offset(y: isAnimating ? -8 : 8) + .animation( + Animation.easeInOut(duration: 0.5) + .repeatForever(autoreverses: true), + value: isAnimating + ) + } + + private var loadingText: some View { + Text("Loading...") + .foregroundStyle(Constants.Colors.white) + .font(Constants.Fonts.f1) + .scaleEffect(isAnimating ? 1.05 : 1.0) + .opacity(isAnimating ? 1.0 : 0.5) + .animation( + Animation.easeInOut(duration: 0.5) + .repeatForever(autoreverses: true), + value: isAnimating + ) + } + +} diff --git a/Uplift/Utils/Custom/LoadingModifier.swift b/Uplift/Utils/Custom/LoadingModifier.swift new file mode 100644 index 0000000..b4d38b4 --- /dev/null +++ b/Uplift/Utils/Custom/LoadingModifier.swift @@ -0,0 +1,36 @@ +// +// LoadingModifier.swift +// Uplift +// +// Created by jiwon jeong on 9/24/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +struct LoadingModifier: ViewModifier { + let isLoading: Bool + let loadingView: LoadingView + + func body(content: Content) -> some View { + ZStack { + content + .disabled(isLoading) + .blur(radius: isLoading ? 2 : 0) + + if isLoading { + loadingView + } + } + .animation(.easeInOut, value: isLoading) + } +} + +extension View { + func loading( + _ isLoading: Bool, + @ViewBuilder loadingView: () -> LoadingView + ) -> some View { + self.modifier(LoadingModifier(isLoading: isLoading, loadingView: loadingView())) + } +} diff --git a/Uplift/Utils/Custom/ModalModifier.swift b/Uplift/Utils/Custom/ModalModifier.swift new file mode 100644 index 0000000..c1941e5 --- /dev/null +++ b/Uplift/Utils/Custom/ModalModifier.swift @@ -0,0 +1,54 @@ +// +// ModalModifier.swift +// Uplift +// +// Created by jiwon jeong on 10/1/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +struct ModalModifier: ViewModifier { + @Binding var showModal: Bool + let modalContent: () -> ModalContent + + func body(content: Content) -> some View { + ZStack { + content + .disabled(showModal) + .blur(radius: showModal ? 2 : 0) + + if showModal { + ZStack { + // Dark background overlay + Color.black.opacity(0.4) + .ignoresSafeArea() + .onTapGesture { + withAnimation(.easeInOut(duration: 0.3)) { + showModal = false + } + } + + VStack { + modalContent() + } + .background(Color.white) + .cornerRadius(16) + .shadow(radius: 10) + .transition(.scale.combined(with: .opacity)) + } + .transition(.opacity) + } + } + .animation(.easeInOut, value: showModal) + } +} + +extension View { + func showModal( + _ showModal: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ModalModifier(showModal: showModal, modalContent: content)) + } +} diff --git a/Uplift/ViewModels/CapacityRemindersViewModel.swift b/Uplift/ViewModels/CapacityRemindersViewModel.swift index bf7315f..1ec2109 100644 --- a/Uplift/ViewModels/CapacityRemindersViewModel.swift +++ b/Uplift/ViewModels/CapacityRemindersViewModel.swift @@ -2,15 +2,20 @@ // CapacityRemindersViewModel.swift // Uplift // -// Created by Caitlyn Jin on 9/27/24. +// Created by Jiwon Jeong on 9/25/25. // Copyright © 2024 Cornell AppDev. All rights reserved. // +import Combine import Foundation +import OSLog +import UpliftAPI +import SwiftUI +import FirebaseMessaging extension CapacityRemindersView { - /// The ViewModel for the Classes page view. + /// The ViewModel for the Capacity Reminders view. @MainActor class ViewModel: ObservableObject { @@ -18,6 +23,305 @@ extension CapacityRemindersView { @Published var selectedDays: [DayOfWeek] = [] @Published var selectedLocations: [String] = [] + @Published var capacityThreshold: Double = 50.0 + @Published var savedReminderId: Int? + @Published var creatingReminder: Bool = false + @Published var deletingReminder: Bool = false + @Published var editingReminder: Bool = false + + @Published var showInfo = false + @Published var fcmToken: String = "" + + @Published var showUnsavedChangesModal = false + @Published var hasUnsavedChanges: Bool = false + + @Published var originalSelectedDays: [DayOfWeek] = [] + @Published var originalCapacityThreshold: Double = 50 + @Published var originalSelectedLocations: [String] = [] + @Published var originalShowInfo: Bool = false + + @Published var isLoading: Bool = false + + var onDismiss: (() -> Void)? + + private var queryBag = Set() + + init(savedReminderId: Int? = nil) { + if let id = savedReminderId { + self.savedReminderId = id + loadSavedSelections() + self.showInfo = true + self.originalShowInfo = true + } else { + self.showInfo = false + self.originalShowInfo = false + } + } + + // MARK: - Functions + + func cleanupLocalReminderData() { + self.savedReminderId = nil + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.reminderId) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.selectedDays) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.selectedLocations) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.capacityThreshold) + Logger.data.info("Cleaned up local reminder data due to remote not found error") + } + + /// retrieves the FCM token + func getFCMToken() { + Messaging.messaging().token { token, error in + if let error = error { + Logger.data.info("Error getting FCM token: \(error.localizedDescription)") + } else if let token = token { + Logger.data.info("FCM TOKEN: \(token)") + self.fcmToken = token + UIPasteboard.general.string = token + } + } + } + + /// checks for unsaved changes + func checkForUnsavedChanges() { + hasUnsavedChanges = ( + showInfo != originalShowInfo || + selectedDays != originalSelectedDays || + capacityThreshold != originalCapacityThreshold || + selectedLocations != originalSelectedLocations + ) + } + + /// saves original values when needed + func saveOriginalValues() { + originalSelectedDays = selectedDays + originalCapacityThreshold = capacityThreshold + originalSelectedLocations = selectedLocations + originalShowInfo = showInfo + } + + /// creates a default reminder if toggle is on; if off, deletes it from the local storage + func handleToggleChange(isOn: Bool) { + if isOn { + if savedReminderId == nil { + createDefaultReminder() + } + } else { + if savedReminderId != nil { + deleteCapacityReminder() + } + } + checkForUnsavedChanges() + } + + /// creates a default reminder + func createDefaultReminder() { + let daysOfWeekStrings = selectedDays.map { $0.dayOfWeekComplete().uppercased() } + + createCapacityReminder( + capacityPercent: Int(capacityThreshold), + daysOfWeek: daysOfWeekStrings, + fcmToken: fcmToken, + gyms: selectedLocations + ) + } + + /// edits the device's reminder + func saveReminder(onComplete: (() -> Void)? = nil) { + showUnsavedChangesModal = false + + if savedReminderId != nil { + let daysOfWeekStrings = selectedDays.map { $0.dayOfWeekComplete().uppercased() } + + editCapacityReminder( + capacityPercent: Int(capacityThreshold), + daysOfWeek: daysOfWeekStrings, + gyms: selectedLocations, + onComplete: onComplete + ) + } + + saveOriginalValues() + hasUnsavedChanges = false + } + + /// load saved days & gym locations + func loadSavedSelections() { + if let savedDayNumbers = UserDefaults.standard.array(forKey: Constants.UserDefaultsKeys.selectedDays) as? [Int] { + selectedDays = savedDayNumbers.compactMap { DayOfWeek(rawValue: $0) } + } + + if let savedLocations = UserDefaults.standard.array(forKey: Constants.UserDefaultsKeys.selectedLocations) as? [String] { + selectedLocations = savedLocations + } + + if let savedCapacity = UserDefaults.standard.object(forKey: Constants.UserDefaultsKeys.capacityThreshold) as? Double { + capacityThreshold = savedCapacity + } + } + + private func saveDaysToUserDefaults() { + let dayNumbers = selectedDays.map { $0.rawValue } + UserDefaults.standard.set(dayNumbers, forKey: Constants.UserDefaultsKeys.selectedDays) + } + + private func saveLocationsToUserDefaults() { + UserDefaults.standard.set(selectedLocations, forKey: Constants.UserDefaultsKeys.selectedLocations) + } + + private func saveCapacityToUserDefaults(_ capacity: Double) { + UserDefaults.standard.set(capacity, forKey: Constants.UserDefaultsKeys.capacityThreshold) + } + + /// Create a new capacity reminder + func createCapacityReminder( + capacityPercent: Int, + daysOfWeek: [String], + fcmToken: String, + gyms: [String] + ) { + isLoading = true + + creatingReminder = true + + saveCapacityToUserDefaults(Double(capacityPercent)) + + let mutation = UpliftAPI.CreateCapacityReminderMutation( + capacityPercent: capacityPercent, + daysOfWeek: daysOfWeek, + fcmToken: fcmToken, + gyms: gyms + ) + + Network.client.mutationPublisher(mutation: mutation) + .map { result -> Int? in + if let idString = result.data?.createCapacityReminder?.id, + let id = Int(idString) { + return id + } + return nil + } + .sink { completion in + if case let .failure(error) = completion { + Logger.data.critical("Error in creating capacity reminder: \(error)") + } + self.creatingReminder = false + } receiveValue: { [weak self] reminderId in + guard let self, let id = reminderId else { return } + + self.savedReminderId = id + + UserDefaults.standard.set(id, forKey: Constants.UserDefaultsKeys.reminderId) + + self.saveDaysToUserDefaults() + self.saveLocationsToUserDefaults() + + Logger.data.info("Successfully created capacity reminder with ID: \(id)") + self.isLoading = false + self.creatingReminder = false + } + .store(in: &queryBag) + } + + /// Edit an existing capacity reminder + func editCapacityReminder( + capacityPercent: Int, + daysOfWeek: [String], + gyms: [String], + onComplete: (() -> Void)? = nil + ) { + guard savedReminderId != nil else { + Logger.data.error("Cannot edit reminder: no saved reminder ID") + return + } + + isLoading = true + + editingReminder = true + + saveCapacityToUserDefaults(Double(capacityPercent)) + + let mutation = UpliftAPI.EditCapacityReminderMutation( + capacityPercent: capacityPercent, + daysOfWeek: daysOfWeek, + gyms: gyms, + reminderId: savedReminderId! + ) + + Network.client.mutationPublisher(mutation: mutation) + .map { result -> Int? in + if let idString = result.data?.editCapacityReminder?.id, + let id = Int(idString) { + return id + } + + return nil + } + .sink { completion in + if case let .failure(error) = completion { + Logger.data.critical("Error in editing capacity reminder: \(error)") + } + + self.editingReminder = false + } receiveValue: { [weak self] reminderId in + guard let self, let id = reminderId else { return } + + self.saveDaysToUserDefaults() + self.saveLocationsToUserDefaults() + + Logger.data.info("Successfully edited capacity reminder with ID: \(id)") + self.isLoading = false + self.editingReminder = false + onComplete?() + } + .store(in: &queryBag) + } + + /// Delete a capacity reminder + func deleteCapacityReminder() { + guard savedReminderId != nil else { + Logger.data.error("Cannot delete reminder: no saved reminder ID") + return + } + + isLoading = true + + deletingReminder = true + + let mutation = UpliftAPI.DeleteCapacityReminderMutation( + reminderId: savedReminderId! + ) + + Network.client.mutationPublisher(mutation: mutation) + .map { result -> Int? in + if let idString = result.data?.deleteCapacityReminder?.id, + let id = Int(idString) { + return id + } + return nil + } + .sink { completion in + if case let .failure(error) = completion { + Logger.data.critical("Error in deleting capacity reminder: \(error)") + } + self.deletingReminder = false + } receiveValue: { [weak self] reminderId in + guard let self, let id = reminderId else { return } + + self.savedReminderId = nil + + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.reminderId) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.selectedDays) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.selectedLocations) + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.capacityThreshold) + + Logger.data.info("Successfully deleted capacity reminder with ID: \(id)") + + self.isLoading = false + self.deletingReminder = false + } + .store(in: &queryBag) + } } } diff --git a/Uplift/Views/HomeView.swift b/Uplift/Views/HomeView.swift index 5cc46da..bea4eed 100644 --- a/Uplift/Views/HomeView.swift +++ b/Uplift/Views/HomeView.swift @@ -88,6 +88,31 @@ struct HomeView: View { .transition(.move(edge: .top)) } + private var capacityReminder: some View { + NavigationLink { + CapacityRemindersView() + } label: { + HStack { + Text("CAPACITY REMINDERS") + .font(Constants.Fonts.bodyMedium) + .foregroundStyle(Constants.Colors.gray03) + .padding(.vertical, 12) + + Spacer() + + Image(systemName: "square.and.pencil") + .foregroundStyle(Constants.Colors.gray03) + } + .padding(.horizontal, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Constants.Colors.white) + .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2) + ) + } + .buttonStyle(ScaleButtonStyle()) + } + private func capacityCircle(facility: Facility?) -> some View { VStack(spacing: 12) { if let facility { @@ -130,7 +155,13 @@ struct HomeView: View { private var scrollContent: some View { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: 12) { - viewModel.showCapacities ? capacitiesView : nil + if viewModel.showCapacities { + VStack(alignment: .leading, spacing: 12) { + capacitiesView + capacityReminder + } + .transition(.move(edge: .top)) + } // TODO: Uncomment to display giveaway modal // giveawayModalCell @@ -251,4 +282,5 @@ struct HomeView: View { #Preview { HomeView(popUpGiveaway: .constant(false)) + .environmentObject(LocationManager.shared) } diff --git a/Uplift/Views/Profile/CapacityRemindersView.swift b/Uplift/Views/Profile/CapacityRemindersView.swift index 04f5e0e..4b45ca3 100644 --- a/Uplift/Views/Profile/CapacityRemindersView.swift +++ b/Uplift/Views/Profile/CapacityRemindersView.swift @@ -16,12 +16,12 @@ struct CapacityRemindersView: View { @StateObject private var viewModel = ViewModel() @Environment(\.dismiss) private var dismiss - @State private var capacity = 50.0 - @State private var showInfo = false - // MARK: - Constants - - private let gyms = ["Teagle Up", "Teagle Down", "Helen Newman", "Toni Morrison", "Noyes"] + init() { + if let savedId = UserDefaults.standard.object(forKey: Constants.UserDefaultsKeys.reminderId) as? Int { + _viewModel = StateObject(wrappedValue: ViewModel(savedReminderId: savedId)) + } + } // MARK: - UI @@ -36,10 +36,57 @@ struct CapacityRemindersView: View { .toolbarBackground(.hidden, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarLeading) { - NavBackButton(color: Constants.Colors.black, dismiss: dismiss) + Button(action: { + viewModel.checkForUnsavedChanges() + if !viewModel.hasUnsavedChanges { + dismiss() + } else { + viewModel.showUnsavedChangesModal = true + } + }) { + Constants.Images.arrowLeft + .resizable() + .scaledToFill() + .foregroundStyle(Constants.Colors.black) + .frame(width: 24, height: 24) + } } } .background(Constants.Colors.white) + .onAppear { + viewModel.getFCMToken() + viewModel.saveOriginalValues() + } + } + .showModal($viewModel.showUnsavedChangesModal) { + UnsavedChangesModal( + onSaveChanges: { + viewModel.saveReminder { + withAnimation(.easeInOut(duration: 0.3)) { + dismiss() + } + } + }, + onContinue: { + withAnimation(.easeInOut(duration: 0.3)) { + viewModel.showInfo = viewModel.originalShowInfo + viewModel.selectedDays = viewModel.originalSelectedDays + viewModel.capacityThreshold = viewModel.originalCapacityThreshold + viewModel.selectedLocations = viewModel.originalSelectedLocations + viewModel.hasUnsavedChanges = false + viewModel.showUnsavedChangesModal = false + dismiss() + } + }, + onCancel: { + withAnimation(.easeInOut(duration: 0.3)) { + viewModel.showUnsavedChangesModal = false + } + } + ) + } + .loading(viewModel.isLoading) { + CustomLoadingView() } } @@ -70,8 +117,11 @@ struct CapacityRemindersView: View { .foregroundStyle(Constants.Colors.gray04) .font(Constants.Fonts.h3) - Toggle("", isOn: $showInfo.animation()) + Toggle("", isOn: $viewModel.showInfo.animation()) .tint(Constants.Colors.yellow) + .onChange(of: viewModel.showInfo) { newValue in + viewModel.handleToggleChange(isOn: newValue) + } } Text("Uplift will send you a notification when gyms dip below the set capacity") @@ -80,7 +130,7 @@ struct CapacityRemindersView: View { .multilineTextAlignment(.leading) } - showInfo ? reminderInfo : nil + viewModel.showInfo ? reminderInfo : nil Spacer() } @@ -99,6 +149,7 @@ struct CapacityRemindersView: View { reminderDays capacityThreshold locationsToRemind + saveButton } .padding(.vertical, 16) } @@ -130,6 +181,7 @@ struct CapacityRemindersView: View { } else { viewModel.selectedDays.append(day) } + viewModel.checkForUnsavedChanges() } } } @@ -153,7 +205,7 @@ struct CapacityRemindersView: View { GeometryReader { geometry in HStack { - Text("\(Int(capacity))%") + Text("\(Int(viewModel.capacityThreshold))%") .foregroundStyle(Constants.Colors.gray04) .font(Constants.Fonts.bodySemibold) .padding(10) @@ -161,19 +213,22 @@ struct CapacityRemindersView: View { RoundedRectangle(cornerRadius: 12) .fill(Constants.Colors.gray00) } - .position(x: capacity / 99 * (geometry.size.width - 32) + 16) + .position(x: viewModel.capacityThreshold / 99 * (geometry.size.width - 32) + 16) } } .fixedSize(horizontal: false, vertical: true) .padding(.vertical, 12) Slider( - value: $capacity, + value: $viewModel.capacityThreshold, in: 0...100, step: 10 ) .tint(Constants.Colors.yellow) .frame(height: 8) + .onChange(of: viewModel.capacityThreshold) { _ in + viewModel.checkForUnsavedChanges() + } HStack { Text("0%") @@ -198,41 +253,57 @@ struct CapacityRemindersView: View { Spacer() } - WrappingHStack(gyms, id: \.self, spacing: .constant(16)) { gym in - Text(gym) + WrappingHStack(GymIdentifier.allCases, id: \.self, spacing: .constant(16)) { gym in + Text(gym.displayName()) .foregroundStyle(Constants.Colors.gray04) .font(Constants.Fonts.labelSemibold) .padding(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) .background { RoundedRectangle(cornerRadius: 8) .fill( - viewModel.selectedLocations.contains(gym) - ? Constants.Colors.lightYellow - : Constants.Colors.white + viewModel.selectedLocations.contains(gym.rawValue) + ? Constants.Colors.lightYellow + : Constants.Colors.white ) } .overlay { RoundedRectangle(cornerRadius: 8) .stroke( - viewModel.selectedLocations.contains(gym) - ? Constants.Colors.yellow - : Constants.Colors.gray02, + viewModel.selectedLocations.contains(gym.rawValue) + ? Constants.Colors.yellow + : Constants.Colors.gray02, lineWidth: 1 ) } .padding(.bottom, 12) .onTapGesture { withAnimation { - if viewModel.selectedLocations.contains(gym) { - viewModel.selectedLocations.removeAll { $0 == gym } + if viewModel.selectedLocations.contains(gym.rawValue) { + viewModel.selectedLocations.removeAll { $0 == gym.rawValue } } else { - viewModel.selectedLocations.append(gym) + viewModel.selectedLocations.append(gym.rawValue) } + viewModel.checkForUnsavedChanges() } } } } } + + private var saveButton: some View { + Button { + viewModel.saveReminder() + } label: { + Text("Save Changes") + .frame(width: 165, height: 41) + .foregroundStyle(Constants.Colors.white) + .font(Constants.Fonts.h3) + .background( + RoundedRectangle(cornerRadius: 30) + .fill(Constants.Colors.black) + ) + } + } } #Preview { diff --git a/Uplift/Views/Supporting/UnsavedChangesModal.swift b/Uplift/Views/Supporting/UnsavedChangesModal.swift new file mode 100644 index 0000000..f6663fd --- /dev/null +++ b/Uplift/Views/Supporting/UnsavedChangesModal.swift @@ -0,0 +1,68 @@ +// +// UnsavedChangesModal.swift +// Uplift +// +// Created by jiwon jeong on 9/19/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +/// A modal for the unsaved changes popup. +struct UnsavedChangesModal: View { + let onSaveChanges: () -> Void + let onContinue: () -> Void + let onCancel: () -> Void + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(spacing: 16) { + Constants.Images.pencil + .resizable() + .frame(width: 40, height: 40) + + Text("Your unsaved changes will be lost. Save before closing?") + .font(Constants.Fonts.f3) + .multilineTextAlignment(.center) + .foregroundColor(Constants.Colors.black) + .padding(.horizontal, 20) + + Button { + onSaveChanges() + } label: { + Text("Save") + .frame(width: 209, height: 41) + .foregroundStyle(Constants.Colors.white) + .font(Constants.Fonts.h3) + .background( + RoundedRectangle(cornerRadius: 30) + .fill(Constants.Colors.black) + ) + } + + Button { + onContinue() + } label: { + Text("Continue") + .foregroundStyle(Constants.Colors.black) + .font(Constants.Fonts.h3) + } + } + + Button { + onCancel() + } label: { + Constants.Images.crossThin + .foregroundColor(Constants.Colors.black) + .frame(width: 32, height: 32) + } + .offset(x: -10, y: -10) + } + .frame(width: 249, height: 242) + } +} + +#Preview { + UnsavedChangesModal(onSaveChanges: {}, onContinue: {}, onCancel: {}) + .background(Constants.Colors.black) +}