diff --git a/Uplift.xcodeproj/project.pbxproj b/Uplift.xcodeproj/project.pbxproj index 314ddd8..b7d033d 100644 --- a/Uplift.xcodeproj/project.pbxproj +++ b/Uplift.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 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 */; }; @@ -123,6 +129,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; @@ -261,6 +273,9 @@ 2E090EC62B12FB9F00BAE982 /* Models */ = { isa = PBXGroup; children = ( + 291433882D8738A200F913D5 /* WorkoutHistory.swift */, + 291433862D87388800F913D5 /* WeeklyWorkoutData.swift */, + 291433842D87387500F913D5 /* UserProfile.swift */, 2E15F4D92B39102A00414BEC /* Capacity.swift */, 2E6785C32B3A780600DD3ADA /* DayOfWeek.swift */, 63A7ABCE2B8C119A008D58FB /* Equipment.swift */, @@ -317,6 +332,8 @@ 2E090ED02B1308C900BAE982 /* Views */ = { isa = PBXGroup; children = ( + 294CC21C2D7E34D000EF6487 /* WeeklyWorkoutTrackerView.swift */, + 293032562D7A8F64002E5484 /* WorkoutProgressArc.swift */, 89950D892B992E8400DFB007 /* ClassesView.swift */, 63CA15862C924B84001620B5 /* Onboarding */, 2E8FE3912B1278B700B3DC6A /* HomeView.swift */, @@ -341,6 +358,7 @@ 2E6785C12B3A5CE000DD3ADA /* GymDetailViewModel.swift */, 2E1105BE2B13B0E100119F5B /* HomeViewModel.swift */, 2E2748CE2BCD4D1F0023882E /* MainViewModel.swift */, + 296A2DAD2D7A805300EF042F /* ProfileViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -756,6 +774,8 @@ 2E6785B92B3A425E00DD3ADA /* GymDetailView.swift in Sources */, 893AD9152D59569600C0817B /* MuscleCategoryView.swift in Sources */, 89C8658D2BB4779C00758337 /* ClassCell.swift in Sources */, + 291433852D87387B00F913D5 /* UserProfile.swift in Sources */, + 294CC21D2D7E34D300EF6487 /* WeeklyWorkoutTrackerView.swift in Sources */, 2E1105BF2B13B0E100119F5B /* HomeViewModel.swift in Sources */, 893AD9172D595DC800C0817B /* MuscleGroupView.swift in Sources */, 89E4FAAA2CEFEC3C00A952B1 /* CapacitySemiCircleView.swift in Sources */, @@ -764,6 +784,7 @@ 2E8FE3922B1278B700B3DC6A /* HomeView.swift in Sources */, 2E6785C02B3A5CA300DD3ADA /* Haptics.swift in Sources */, 89950D8A2B992E8400DFB007 /* ClassesView.swift in Sources */, + 296A2DAE2D7A805300EF042F /* ProfileViewModel.swift in Sources */, 2E15F50C2B3A0B2100414BEC /* CapacityCircleView.swift in Sources */, 2E090EC52B12EF2600BAE982 /* Publishers.swift in Sources */, 2E39D8222B3B631200AD238B /* DividerLine.swift in Sources */, @@ -780,6 +801,7 @@ 2E2748CF2BCD4D1F0023882E /* MainViewModel.swift in Sources */, 2E5726C72B4A63AE00D3DB36 /* ShimmerModifier.swift in Sources */, 2E15F5022B396EC500414BEC /* ShadowModifier.swift in Sources */, + 291433872D87388C00F913D5 /* WeeklyWorkoutData.swift in Sources */, 2E15F4DA2B39102A00414BEC /* Capacity.swift in Sources */, 2ED53DCF2B3E06DC00FBDEAB /* Logger.swift in Sources */, 2E15F4E62B391E5300414BEC /* Array+Extension.swift in Sources */, @@ -789,6 +811,7 @@ 897703662BA2028D00F9992F /* ClassesViewModel.swift in Sources */, 2E1105C22B13D15100119F5B /* HomeGymCell.swift in Sources */, 2E15F4F92B3950F800414BEC /* Date+Extension.swift in Sources */, + 293032572D7A8F64002E5484 /* WorkoutProgressArc.swift in Sources */, 2E8FE3902B1278B700B3DC6A /* UpliftApp.swift in Sources */, 2EE5F3C82B12E094008E0299 /* ApolloClientProtocol.swift in Sources */, 2E39D8262B3B6C6500AD238B /* SlidingTabBarView.swift in Sources */, @@ -801,6 +824,7 @@ 89A652F92D02B00000277A16 /* CapacityRemindersView.swift in Sources */, 89A652FA2D02B00000277A16 /* RemindersView.swift in Sources */, 2E39D81E2B3B610200AD238B /* UINavigationController+Extension.swift in Sources */, + 291433892D8738A400F913D5 /* WorkoutHistory.swift in Sources */, 63001AD62CC9ACFF0082AFFA /* LoginViewModel.swift in Sources */, 2E15F4DC2B39117300414BEC /* OpenHours.swift in Sources */, 2E090ED62B13121600BAE982 /* Constants.swift in Sources */, diff --git a/Uplift/Models/UserProfile.swift b/Uplift/Models/UserProfile.swift new file mode 100644 index 0000000..6061490 --- /dev/null +++ b/Uplift/Models/UserProfile.swift @@ -0,0 +1,14 @@ +// +// UserProfile.swift +// Uplift +// +// Created by jiwon jeong on 3/16/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import Foundation + +struct UserProfile { + let id: String + let name: String +} diff --git a/Uplift/Models/WeeklyWorkoutData.swift b/Uplift/Models/WeeklyWorkoutData.swift new file mode 100644 index 0000000..fdbcb43 --- /dev/null +++ b/Uplift/Models/WeeklyWorkoutData.swift @@ -0,0 +1,19 @@ +// +// WeeklyWorkoutData.swift +// Uplift +// +// Created by jiwon jeong on 3/16/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import Foundation + +struct WeeklyWorkoutData { + var currentWeekWorkouts: Int + var weeklyGoal: Int + var weekDates: [Date] + + var progressPercentage: Double { + Double(currentWeekWorkouts) / Double(weeklyGoal) + } +} diff --git a/Uplift/Models/WorkoutHistory.swift b/Uplift/Models/WorkoutHistory.swift new file mode 100644 index 0000000..72864be --- /dev/null +++ b/Uplift/Models/WorkoutHistory.swift @@ -0,0 +1,16 @@ +// +// WorkoutHistory.swift +// Uplift +// +// Created by jiwon jeong on 3/16/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import Foundation + +struct WorkoutHistory: Identifiable { + let id: String + let location: String + let time: String + let date: String +} diff --git a/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Contents.json new file mode 100644 index 0000000..f469169 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Profile.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Profile 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Profile 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 1.png b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 1.png new file mode 100644 index 0000000..2f601b6 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 1.png differ diff --git a/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 2.png b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 2.png new file mode 100644 index 0000000..5450c35 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile 2.png differ diff --git a/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile.png b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile.png new file mode 100644 index 0000000..9dfff80 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_outline.imageset/Profile.png differ diff --git a/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Contents.json new file mode 100644 index 0000000..eb645e7 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Profile.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Profile-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Profile-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-2.png b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-2.png new file mode 100644 index 0000000..faa4e79 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-2.png differ diff --git a/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-3.png b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-3.png new file mode 100644 index 0000000..55ebb34 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile-3.png differ diff --git a/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile.png b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile.png new file mode 100644 index 0000000..2b696dc Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/profile_solid.imageset/Profile.png differ diff --git a/Uplift/Resources/Assets.xcassets/settings.imageset/Contents.json b/Uplift/Resources/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000..55dbd31 --- /dev/null +++ b/Uplift/Resources/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Settings.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Settings 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Settings 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 1.png b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 1.png new file mode 100644 index 0000000..874cde1 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 1.png differ diff --git a/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 2.png b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 2.png new file mode 100644 index 0000000..6684b9e Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings 2.png differ diff --git a/Uplift/Resources/Assets.xcassets/settings.imageset/Settings.png b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings.png new file mode 100644 index 0000000..e3c8b71 Binary files /dev/null and b/Uplift/Resources/Assets.xcassets/settings.imageset/Settings.png differ diff --git a/Uplift/Utils/Constants.swift b/Uplift/Utils/Constants.swift index 052f1ef..0c93094 100644 --- a/Uplift/Utils/Constants.swift +++ b/Uplift/Utils/Constants.swift @@ -92,6 +92,8 @@ struct Constants { static let labelMedium = Font.custom("Montserrat-Medium", size: 12) static let labelSemibold = Font.custom("Montserrat-SemiBold", size: 12) static let labelBold = Font.custom("Montserrat-Bold", size: 12) + + static let p1 = Font.custom("Montserrat-Bold", size: 64) } /// Image components used in Uplift. @@ -113,9 +115,11 @@ struct Constants { static let chest = Image("chest") static let clock = Image("clock") 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 dumbbellSolid = Image("dumbbell_solid") + static let profileOutline = Image("profile_outline") + static let profileSolid = Image("profile_solid") static let elevator = Image("elevator") static let greenTea = Image("green_tea") static let giveawayModalBackground = Image("giveaway_modal_bg") @@ -136,6 +140,8 @@ struct Constants { static let vertEllipsis = Image("vert_ellipsis") static let whistleOutline = Image("whistle_outline") static let whistleSolid = Image("whistle_solid") + static let settings = Image("settings") + static let profileEmpty = Image("profile_empty") } /// Padding amounts used in Uplift. diff --git a/Uplift/Utils/DummyData.swift b/Uplift/Utils/DummyData.swift index 16b66f3..8170795 100644 --- a/Uplift/Utils/DummyData.swift +++ b/Uplift/Utils/DummyData.swift @@ -710,4 +710,67 @@ struct DummyData { ] ] + /// Dummy data for profile view. + struct ProfileViewData { + + // Profile data + static let profile = UserProfile( + id: "user123", + name: "Jiwon Jeong" + ) + + static let totalWorkouts = 132 + static let streaks = 14 + static let badges = 6 + + // Create dates for the week + static let weekDates: [Date] = { + [25, 26, 27, 28, 29, 30, 31].map { day -> Date in + var components = DateComponents() + components.year = 2024 + components.month = 3 + components.day = day + return Calendar.current.date(from: components) ?? Date() + } + }() + + static let weeklyWorkouts = WeeklyWorkoutData( + currentWeekWorkouts: 0, + weeklyGoal: 5, + weekDates: weekDates + ) + + static let workoutHistory: [WorkoutHistory] = [ + WorkoutHistory( + id: "workout1", + location: "Helen Newman", + time: "6:30 PM", + date: "Fri Mar 29, 2024" + ), + WorkoutHistory( + id: "workout2", + location: "Teagle Up", + time: "7:15 PM", + date: "Thu Mar 28, 2024" + ), + WorkoutHistory( + id: "workout3", + location: "Helen Newman", + time: "6:32 PM", + date: "Tue Mar 26, 2024" + ), + WorkoutHistory( + id: "workout4", + location: "Toni Morrison", + time: "7:37 PM", + date: "Sun Mar 24, 2024" + ), + WorkoutHistory( + id: "workout5", + location: "Helen Newman", + time: "10:02 AM", + date: "Sat Mar 23, 2024" + ) + ] + } } diff --git a/Uplift/Utils/Extensions/Date+Extension.swift b/Uplift/Utils/Extensions/Date+Extension.swift index 7b9bdfb..7bf790e 100644 --- a/Uplift/Utils/Extensions/Date+Extension.swift +++ b/Uplift/Utils/Extensions/Date+Extension.swift @@ -85,4 +85,10 @@ extension Date { return thisDate.month == otherDate.month && thisDate.day == otherDate.day } + /// Creates a Date object from a string with the specified format. + static func fromString(_ dateString: String, format: String = "EEE MMM dd, yyyy") -> Date? { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.date(from: dateString) + } } diff --git a/Uplift/ViewModels/ProfileViewModel.swift b/Uplift/ViewModels/ProfileViewModel.swift new file mode 100644 index 0000000..6420f38 --- /dev/null +++ b/Uplift/ViewModels/ProfileViewModel.swift @@ -0,0 +1,44 @@ +// +// ProfileViewModel.swift +// Uplift +// +// Created by jiwon jeong on 3/6/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import Foundation + +// MARK: - ViewModel +extension ProfileView { + class ViewModel: ObservableObject { + @Published var profile: UserProfile? + @Published var showSettingsSheet = false + @Published var workoutHistory: [WorkoutHistory] = [] + @Published var weeklyWorkouts: WeeklyWorkoutData = WeeklyWorkoutData( + currentWeekWorkouts: 0, + weeklyGoal: 5, + weekDates: [] + ) + @Published var totalWorkouts: Int = 0 + @Published var streaks: Int = 14 + @Published var badges: Int = 6 + + /// dummy data + func fetchUserProfile() { + self.profile = DummyData.ProfileViewData.profile + self.totalWorkouts = DummyData.ProfileViewData.totalWorkouts + self.streaks = DummyData.ProfileViewData.streaks + self.badges = DummyData.ProfileViewData.badges + self.weeklyWorkouts = DummyData.ProfileViewData.weeklyWorkouts + self.workoutHistory = DummyData.ProfileViewData.workoutHistory + } + + private func createDate(day: Int) -> Date { + var components = DateComponents() + components.year = 2024 + components.month = 3 + components.day = day + return Calendar.current.date(from: components) ?? Date() + } + } +} diff --git a/Uplift/Views/MainView.swift b/Uplift/Views/MainView.swift index 64f7f52..3568b4a 100644 --- a/Uplift/Views/MainView.swift +++ b/Uplift/Views/MainView.swift @@ -30,6 +30,7 @@ struct MainView: View { .environmentObject(tabBarProp) case .profile: ProfileView() + .environmentObject(tabBarProp) } } .overlay(alignment: .bottom) { @@ -116,7 +117,7 @@ struct MainView: View { selectedTab = .profile } label: { tabItemView( - icon: selectedTab == .profile ? Constants.Images.whistleSolid : Constants.Images.whistleOutline, + icon: selectedTab == .profile ? Constants.Images.profileSolid : Constants.Images.profileOutline, name: "Profile" ) } diff --git a/Uplift/Views/ProfileView.swift b/Uplift/Views/ProfileView.swift index 91cf8e4..577353a 100644 --- a/Uplift/Views/ProfileView.swift +++ b/Uplift/Views/ProfileView.swift @@ -2,30 +2,334 @@ // ProfileView.swift // Uplift // -// Created by Caitlyn Jin on 9/26/24. -// Copyright © 2024 Cornell AppDev. All rights reserved. +// Created by jiwon jeong on 2/27/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. // import SwiftUI +import Kingfisher /// The main view for the Profile page. struct ProfileView: View { + // MARK: - Properties + @EnvironmentObject var tabBarProp: TabBarProperty + @StateObject private var viewModel = ViewModel() + + private let radius = 125 + + // MARK: - UI var body: some View { NavigationStack { VStack { - NavigationLink { - RemindersView() + header + scrollContent + } + .background(Constants.Colors.white) + } + .onAppear { + viewModel.fetchUserProfile() + } + } + + private var header: some View { + VStack { + Spacer() + + HStack { + Text("Profile") + .foregroundStyle(Constants.Colors.black) + .font(Constants.Fonts.h1) + + Spacer() + + settingsButton + } + } + .padding(.bottom, 12) + .padding(.horizontal, Constants.Padding.homeHorizontal) + .background( + Constants.Colors.white + .upliftShadow(Constants.Shadows.smallLight) + ) + .ignoresSafeArea(.all) + .frame(height: 64) + } + + private var settingsButton: some View { + HStack(spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "star.fill") + .foregroundStyle(Constants.Colors.yellow) + + Text("Favorites") + .font(Constants.Fonts.bodyLight) + .foregroundStyle(Constants.Colors.black) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Constants.Colors.white) + .cornerRadius(20) + .overlay { + RoundedRectangle(cornerRadius: 20) + .stroke(Constants.Colors.yellow, lineWidth: 1) + } + + Button { + viewModel.showSettingsSheet = true + } label: { + Constants.Images.settings + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + .foregroundStyle(Constants.Colors.black) + } + .sheet(isPresented: $viewModel.showSettingsSheet) { + settingsView + } + } + } + + private var settingsView: some View { + VStack(alignment: .leading, spacing: 24) { + HStack { + Text("Settings") + .font(Constants.Fonts.h1) + .foregroundStyle(Constants.Colors.black) + + Spacer() + + Button { + viewModel.showSettingsSheet = false } label: { + Constants.Images.cross + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundStyle(Constants.Colors.black) + } + } + .padding(.top, 24) + + DividerLine() + + Button { + //TODO: Learn more about uplift + } label: { + HStack { + Text("About Uplift") + .font(Constants.Fonts.bodyNormal) + .foregroundStyle(Constants.Colors.black) + + Spacer() + } + } + + DividerLine() + + Button { + //TODO: Notifications about uplift + } label: { + HStack { Text("Reminders") + .font(Constants.Fonts.bodyNormal) + .foregroundStyle(Constants.Colors.black) + + Spacer() } } - // TODO: Temporary to allow view to take up whole screen - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Constants.Colors.white) + + DividerLine() + + Button { + //TODO: Reporting an Issue + } label: { + HStack { + Text("Report an Issue") + .font(Constants.Fonts.bodyNormal) + .foregroundStyle(Constants.Colors.black) + + Spacer() + } + } + + DividerLine() + + Button { + //TODO: Logging Out functionality + } label: { + Text("Log Out") + .font(Constants.Fonts.bodyNormal) + .foregroundStyle(Constants.Colors.closed) + } + + Spacer() + } + .padding(.horizontal, 24) + .background(Constants.Colors.white) + } + + private var scrollContent: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 24) { + profileTopSection + goalView + historyView + .padding(.bottom, CGFloat(radius)) + } + .padding(.horizontal, Constants.Padding.homeHorizontal) + .padding(.top, 24) + } + } + + private var profileTopSection: some View { + HStack(spacing: 20) { + // Profile image with camera icon + ZStack(alignment: .bottomTrailing) { + ZStack { + // Outer shadow circle + Circle() + .fill(Constants.Colors.white) + .shadow(color: .gray.opacity(0.5), radius: 3, x: 0, y: 1) + .frame(width: 98, height: 98) + + // White border circle + Circle() + .fill(Constants.Colors.white) + .frame(width: 98, height: 98) + + // Profile image + Image(systemName: "person.crop.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 93, height: 93) + .foregroundStyle(Constants.Colors.gray02) + } + + // Camera button overlay + Circle() + .fill(Constants.Colors.white) + .shadow(color: .gray.opacity(0.5), radius: 3, x: 0, y: 1) + .frame(width: 32, height: 32) + .overlay { + Image(systemName: "camera.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Constants.Colors.gray03) + } + .offset(x: 2, y: 2) + } + + // Name and workouts count + VStack(alignment: .leading, spacing: 16) { + Text(viewModel.profile?.name ?? "Anonymous") + .font(Constants.Fonts.h1) + .foregroundStyle(Constants.Colors.black) + + HStack(spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("\(viewModel.totalWorkouts)") + .font(Constants.Fonts.h2) + .foregroundStyle(Constants.Colors.black) + + Text("Gym Days") + .font(Constants.Fonts.labelMedium) + .foregroundStyle(Constants.Colors.gray04) + .fixedSize(horizontal: false, vertical: true) + } + .frame(minWidth: 70, alignment: .leading) + + VStack(alignment: .leading, spacing: 4) { + Text("\(viewModel.streaks)") + .font(Constants.Fonts.h2) + .foregroundStyle(Constants.Colors.black) + + Text("Streaks") + .font(Constants.Fonts.labelMedium) + .foregroundStyle(Constants.Colors.gray04) + } + .frame(minWidth: 55, alignment: .leading) + + VStack(alignment: .leading, spacing: 4) { + Text("\(viewModel.badges)") + .font(Constants.Fonts.h2) + .foregroundStyle(Constants.Colors.black) + + Text("Badges") + .font(Constants.Fonts.labelMedium) + .foregroundStyle(Constants.Colors.gray04) + } + .frame(minWidth: 55, alignment: .leading) + } + } + + Spacer(minLength: 0) + } + .padding(.horizontal, 2) + } + + private var goalView: some View { + VStack { + HStack { + Text("My Goals") + .font(Constants.Fonts.h2) + .foregroundColor(Constants.Colors.gray04) + + Spacer() + + Image(systemName: "chevron.right") + .resizable() + .frame(width: 8, height: 12) + .foregroundColor(Constants.Colors.gray03) + } + + VStack(spacing: CGFloat(-radius) + 16) { + WorkoutProgressArc(viewModel: viewModel) + WeeklyWorkoutTrackerView(viewModel: viewModel) + } } } + private var historyView: some View { + VStack(spacing: 20) { + HStack { + Text("History") + .font(Constants.Fonts.h2) + .foregroundColor(Constants.Colors.gray04) + + Spacer() + + Image(systemName: "chevron.right") + .resizable() + .frame(width: 8, height: 12) + .foregroundColor(Constants.Colors.gray03) + } + + ForEach(viewModel.workoutHistory.indices, id: \.self) { index in + LazyVStack(spacing: 8) { + HStack { + let workout = viewModel.workoutHistory[index] + Text(workout.location) + .foregroundStyle(Constants.Colors.black) + .font(Constants.Fonts.bodyMedium) + + Spacer() + + Text("\(workout.time) • \(workout.date.description)") + .foregroundStyle(Constants.Colors.black) + .font(Constants.Fonts.labelLight) + } + + if index < viewModel.workoutHistory.count - 1 { + Rectangle() + .fill(Constants.Colors.gray01) + .frame(height: 1) + } + } + } + } + } } #Preview { diff --git a/Uplift/Views/WeeklyWorkoutTrackerView.swift b/Uplift/Views/WeeklyWorkoutTrackerView.swift new file mode 100644 index 0000000..436669a --- /dev/null +++ b/Uplift/Views/WeeklyWorkoutTrackerView.swift @@ -0,0 +1,165 @@ +// +// WeeklyWorkoutTrackerView.swift +// Uplift +// +// Created by jiwon jeong on 3/9/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +struct WeeklyWorkoutTrackerView: View { + + // MARK: - Properties + + @ObservedObject var viewModel: ProfileView.ViewModel + @State private var animationProgress: [Double] = Array(repeating: 0, count: 7) + + // Weekday abbreviations + private let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + + // Days that have workouts + @State private var workoutDays: [Bool] = [false, false, false, false, false, false, false] + + // Animation timing + private let animationDuration: Double = 0.5 + private let delayBetweenDays: Double = 0.3 + + // Circle dimensions + private let circleSize: CGFloat = 24 + private let lineWidth: CGFloat = 2 + private let spacing: CGFloat = 26.5 + private let verticalSpacing: CGFloat = 2 + + // MARK: - UI + + var body: some View { + VStack { + VStack(alignment: .center, spacing: verticalSpacing) { + // Weekday abbreviations + HStack(spacing: spacing) { + ForEach(weekdays.indices, id: \.self) { index in + Text(weekdays[index]) + .font(Constants.Fonts.labelSemibold) + .foregroundColor(Constants.Colors.black) + .frame(width: circleSize) + .lineLimit(1) + .minimumScaleFactor(0.82) + } + } + + // Workout circles with connecting lines + ZStack { + HStack(spacing: 0) { + ForEach(0..<6, id: \.self) { _ in + HStack(spacing: 0) { + Circle() + .fill(Color.clear) + .frame(width: circleSize) + + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(width: spacing, height: lineWidth) + } + } + + Circle() + .fill(Color.clear) + .frame(width: circleSize) + } + + // Workout day circles + HStack(spacing: spacing) { + ForEach(weekdays.indices, id: \.self) { index in + ZStack { + Circle() + .fill(Color(.systemGray6)) + .frame(width: circleSize, height: circleSize) + + if workoutDays[index] { + Circle() + .fill(Constants.Colors.yellow) + .frame(width: circleSize, height: circleSize) + .scaleEffect(animationProgress[index]) + .opacity(animationProgress[index]) + } + + if workoutDays[index] { + Image(systemName: "checkmark") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .scaleEffect(animationProgress[index]) + .opacity(animationProgress[index]) + } + } + } + } + } + + HStack(spacing: spacing) { + ForEach(weekdays.indices, id: \.self) { index in + Text("\(25 + index)") + .font(Constants.Fonts.labelNormal) + .frame(width: circleSize, height: 20) + .foregroundColor(Constants.Colors.black) + } + } + } + } + .padding(.top, 5) + .padding(.bottom, 15) + .onAppear { + if viewModel.workoutHistory.isEmpty { + viewModel.fetchUserProfile() + } + } + .onReceive(viewModel.$workoutHistory) { workouts in + if !workouts.isEmpty { + determineWorkoutDays() + Task { + await animateWorkouts() + } + } + } + } + + /// Determines which days of the week have completed workouts and updates the UI accordingly + private func determineWorkoutDays() { + let workoutDaysSet = Set(viewModel.workoutHistory.compactMap { workout -> Int? in + guard let date = Date.fromString(workout.date) else { return nil } + return Calendar.current.component(.day, from: date) + }) + + weekdays.indices.forEach { index in + let day = 25 + index + workoutDays[index] = workoutDaysSet.contains(day) + } + + // Reset animation progress + animationProgress = Array(repeating: 0, count: 7) + } + + /// Animates the workout day indicators sequentially from left to right with fade-in effect + private func animateWorkouts() async { + for index in weekdays.indices where workoutDays[index] { + // Add delay between animations + try? await Task.sleep(for: .seconds(delayBetweenDays)) + + await MainActor.run { + withAnimation(.easeIn(duration: animationDuration)) { + animationProgress[index] = 1.0 + } + } + } + } +} + +#Preview { + let viewModel = ProfileView.ViewModel() + viewModel.fetchUserProfile() + + return WeeklyWorkoutTrackerView(viewModel: viewModel) + .frame(height: 100) + .padding() + .background(Color.white) +} diff --git a/Uplift/Views/WorkoutProgressArc.swift b/Uplift/Views/WorkoutProgressArc.swift new file mode 100644 index 0000000..64fde07 --- /dev/null +++ b/Uplift/Views/WorkoutProgressArc.swift @@ -0,0 +1,109 @@ +// +// WorkoutProgressArc.swift +// Uplift +// +// Created by jiwon jeong on 3/9/25. +// Copyright © 2025 Cornell AppDev. All rights reserved. +// + +import SwiftUI + +// Unit Circle Animation +struct WorkoutProgressArc: View { + + // MARK: - Properties + + @State private var arcProgress: Double = 0 + @State private var dotRotation: Double = 0 + + @ObservedObject var viewModel: ProfileView.ViewModel + + /// 3/5 - for dummy data for now + let completedWorkouts: Int = 3 + let targetWorkouts: Int = 5 + let radius: CGFloat = 126 + + // MARK: - UI + + var body: some View { + ZStack { + // Background track + Circle() + .trim(from: 0, to: 0.5) + .stroke( + Color.gray.opacity(0.2), + style: StrokeStyle(lineWidth: 12, lineCap: .round) + ) + .frame(width: radius * 2, height: radius * 2) + .rotationEffect(.degrees(180)) + + // Progress arc - yellow portion + Circle() + .trim(from: 0, to: 0.5 * arcProgress) + .stroke( + Constants.Colors.yellow, + style: StrokeStyle(lineWidth: 12, lineCap: .round) + ) + .frame(width: radius * 2, height: radius * 2) + .rotationEffect(.degrees(180)) + + // Yellow dot + ZStack { + Circle() + .fill(Constants.Colors.yellow) + .frame(width: 26, height: 26) + + Circle() + .fill(Color.white) + .frame(width: 12, height: 12) + } + .offset(x: -radius) + .rotationEffect(.degrees(dotRotation)) + .animation(.easeOut(duration: 1.5), value: dotRotation) + + VStack(spacing: 8) { + // Value + HStack(alignment: .lastTextBaseline, spacing: 2) { + Text("\(completedWorkouts)") + .font(Constants.Fonts.p1) + .foregroundColor(.black) + + Text("/ \(targetWorkouts)") + .font(Constants.Fonts.h1) + .foregroundColor(.gray) + .padding(.leading, 2) + } + + // Label + Text("Days this week") + .font(Constants.Fonts.labelNormal) + .foregroundColor(.gray) + } + .offset(y: -40) + } + .frame(width: radius * 2, height: radius * 2) + .onAppear { + // Start with initial values + arcProgress = 0 + dotRotation = 0 + + // Calculate the final rotation based on progress + let finalRotation = 180 * Double(completedWorkouts) / Double(targetWorkouts) + + // Animate both simultaneously + withAnimation(.easeOut(duration: 1.5)) { + arcProgress = Double(completedWorkouts) / Double(targetWorkouts) + dotRotation = finalRotation + } + } + } +} + +#Preview { + let viewModel = ProfileView.ViewModel() + viewModel.fetchUserProfile() + + return WorkoutProgressArc(viewModel: viewModel) + .padding() + .background(Color.white) +}