From 7bb9c0ac441ade8aae3ab66163a325ade73f85ed Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Sun, 8 Mar 2026 00:53:15 -0500 Subject: [PATCH 01/11] Updated schema and User graphqls --- app/src/main/graphql/User.graphql | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/graphql/User.graphql b/app/src/main/graphql/User.graphql index 09439e6..ebfccc9 100644 --- a/app/src/main/graphql/User.graphql +++ b/app/src/main/graphql/User.graphql @@ -1,14 +1,24 @@ fragment userFields on User { id email - name netId + name + encodedImage + activeStreak + maxStreak + streakStart + workoutGoal + lastGoalChange + lastStreak + totalGymDays } fragment workoutFields on Workout { id workoutTime userId + facilityId + gymName } mutation CreateUser($email: String!, $name: String!, $netId: String!) { From 7aeedf91f24a5986d15055d3b207107ff8053da4 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Sun, 8 Mar 2026 00:55:15 -0500 Subject: [PATCH 02/11] Updated UI to match designs --- .../profile/ProfileHeaderSection.kt | 60 +++++-- .../profile/workouts/HistorySection.kt | 113 ++++++++++++-- .../profile/workouts/WeeklyProgressTracker.kt | 16 +- .../profile/workouts/WorkoutProgressArc.kt | 86 +++++++--- .../profile/workouts/WorkoutSection.kt | 41 +++++ .../ui/screens/profile/ProfileScreen.kt | 147 ++++++++---------- 6 files changed, 317 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt index 2a156f5..2fa3f8f 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt @@ -1,14 +1,18 @@ package com.cornellappdev.uplift.ui.components.profile +import android.R.attr.fontFamily +import android.R.attr.fontWeight import android.net.Uri import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface @@ -19,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -34,7 +39,7 @@ fun ProfileHeaderSection( name: String, gymDays: Int, streaks: Int, - badges: Int, + netID: String, profilePictureUri: Uri?, onPhotoSelected: (Uri) -> Unit ){ @@ -48,7 +53,7 @@ fun ProfileHeaderSection( onPhotoSelected = onPhotoSelected, screenType = ScreenType.PROFILE ) - ProfileHeaderInfoDisplay(name, gymDays, streaks, badges) + ProfileHeaderInfoDisplay(name, gymDays, streaks, netID, modifier = Modifier.weight(1f)) } } @@ -57,34 +62,55 @@ private fun ProfileHeaderInfoDisplay( name: String, gymDays: Int, streaks: Int, - badges: Int + netID: String, + modifier: Modifier = Modifier ) { Column( + modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = name, - fontFamily = montserratFamily, - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = name, + modifier = Modifier.weight(1f, fill = false), + fontFamily = montserratFamily, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (netID.isNotBlank()){ + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "($netID)", + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Light, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween ) { ProfileHeaderInfo( label = "Gym Days", amount = gymDays ) + Spacer(modifier = Modifier.width(36.dp)) ProfileHeaderInfo( label = "Streaks", amount = streaks ) - ProfileHeaderInfo( - label = "Badges", - amount = badges - ) +// ProfileHeaderInfo( +// label = "Badges", +// amount = badges +// ) } } } @@ -115,11 +141,11 @@ private fun ProfileHeaderInfo(label: String, amount: Int) { @Composable private fun ProfileHeaderSectionPreview() { ProfileHeaderSection( - name = "John Doe", + name = "Melissa Velasquez", gymDays = 100, streaks = 15, - badges = 3, profilePictureUri = null, - onPhotoSelected = {} + onPhotoSelected = {}, + netID = "mv477" ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt index 2a61434..e97b7d1 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt @@ -1,42 +1,74 @@ package com.cornellappdev.uplift.ui.components.profile.workouts +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.cornellappdev.uplift.R import com.cornellappdev.uplift.ui.components.profile.SectionTitleText import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily +import com.cornellappdev.uplift.util.timeAgoString +import java.util.Calendar data class HistoryItem( val gymName: String, val time: String, - val dayOfWeek: String, val date: String, + val timestamp: Long ) @Composable fun HistorySection( historyItems: List, - onClick : () -> Unit + onClick : () -> Unit, + modifier: Modifier = Modifier ) { Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Center ) { - SectionTitleText("My History", onClick) - HistoryList(historyItems) + SectionTitleText("My Workout History", onClick) + Spacer(modifier = Modifier.height(12.dp)) + if (historyItems.isNotEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + HistoryList(historyItems) + } + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + EmptyHistorySection() + } + } } @@ -60,26 +92,72 @@ private fun HistoryItemRow( ) { val gymName = historyItem.gymName val time = historyItem.time - val dayOfWeek = historyItem.dayOfWeek val date = historyItem.date + val calendar = Calendar.getInstance().apply { + timeInMillis = historyItem.timestamp + } + val ago = calendar.timeAgoString() + Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 12.dp), horizontalArrangement = Arrangement.SpaceBetween ) { + Column(){ + Text( + text = gymName, + fontFamily = montserratFamily, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color.Black + ) + Text( + text = "$date · $time", + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Light, + color = Color.Gray + ) + } Text( - text = gymName, + text = ago, fontFamily = montserratFamily, - fontSize = 14.sp, + fontSize = 12.sp, fontWeight = FontWeight.Medium, color = Color.Black ) + } +} + +@Composable +private fun EmptyHistorySection(){ + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_bag), + contentDescription = null, + modifier = Modifier + .width(64.99967.dp) + .height(50.8181.dp) + + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "No workouts yet.", + fontFamily = montserratFamily, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) Text( - text = "$time · $dayOfWeek $date", + text = "Head to a gym and check in!", fontFamily = montserratFamily, - fontSize = 12.sp, - fontWeight = FontWeight.Light, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = Color.Black ) } @@ -88,12 +166,13 @@ private fun HistoryItemRow( @Preview(showBackground = true) @Composable private fun HistorySectionPreview() { + val now = System.currentTimeMillis() val historyItems = listOf( - HistoryItem("Morrison", "11:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Noyes", "1:00 PM","Fri", "March 29, 2024"), - HistoryItem("Teagle Up", "2:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Teagle Down", "12:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Helen Newman", "10:00 AM", "Fri", "March 29, 2024"), + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now - (1 * 24 * 60 * 60 * 1000) ), + HistoryItem("Noyes", "1:00 PM", "March 29, 2024", now - (3 * 24 * 60 * 60 * 1000)), + HistoryItem("Teagle Up", "2:00 PM", "March 29, 2024", now - (7 * 24 * 60 * 60 * 1000)), + HistoryItem("Teagle Down", "12:00 PM", "March 29, 2024", now - (15 * 24 * 60 * 60 * 1000)), + HistoryItem("Helen Newman", "10:00 AM", "March 29, 2024", now), ) Column( modifier = Modifier diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WeeklyProgressTracker.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WeeklyProgressTracker.kt index 28e1081..5cdf733 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WeeklyProgressTracker.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WeeklyProgressTracker.kt @@ -48,15 +48,23 @@ fun WeeklyProgressTracker( completedDays: List ) { val daysOfWeek = listOf("M", "T", "W", "Th", "F", "Sa", "Su") - val paddedCompletedDays = if (completedDays.size < daysOfWeek.size) { - completedDays + List(daysOfWeek.size - completedDays.size) { false } - } else { - completedDays + + if (daysOfMonth.size < daysOfWeek.size) { + return } + + val paddedCompletedDays = + if (completedDays.size < daysOfWeek.size) { + completedDays + List(daysOfWeek.size - completedDays.size) { false } + } else { + completedDays + } + val lastCompletedIndex = paddedCompletedDays.indexOfLast { it } Box(modifier = Modifier.fillMaxWidth()) { ConnectingLines(daysOfWeek, lastCompletedIndex) + DayProgressCirclesRow( dayProgressList = daysOfWeek.mapIndexed { index, dayOfWeek -> DayProgress( diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt index be32012..a870c28 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt @@ -52,9 +52,16 @@ fun WorkoutProgressArc( workoutsCompleted: Int, workoutGoal: Int, ) { - // Calculate progress percentage - val progress = (workoutsCompleted.toFloat() / workoutGoal.toFloat()).coerceIn(0f, 1f) + val isZero = workoutsCompleted <= 0 || workoutGoal <= 0 + val isComplete = workoutGoal > 0 && workoutsCompleted >= workoutGoal + // Calculate progress percentage + val progress = when { + workoutGoal <= 0 -> 0f + workoutsCompleted <= 0 -> 0f + else -> (workoutsCompleted.toFloat() / workoutGoal.toFloat()) + .coerceIn(0f, 1f) + } // Setup animation val animatedProgress = remember { Animatable(0f) } @@ -74,16 +81,16 @@ fun WorkoutProgressArc( .height(132.dp) ) { // Draw the progress arc - ProgressArc(animatedProgress, workoutsCompleted, workoutGoal) - WorkoutFractionTextSection(workoutsCompleted, workoutGoal) + ProgressArc(animatedProgress, isZero, isComplete) + WorkoutFractionTextSection(workoutsCompleted, workoutGoal, isComplete) } } @Composable private fun ProgressArc( animatedProgress: Animatable, - workoutsCompleted: Int, - workoutGoal: Int + isZero: Boolean, + isComplete: Boolean ) { val startAngle = 180f; val maxSweepAngle = 180f; @@ -112,16 +119,29 @@ private fun ProgressArc( // Progress arc val progressAngle = maxSweepAngle * animatedProgress.value - drawProgressArc( - workoutsCompleted, - workoutGoal, - gradientBrush, - startAngle, - progressAngle, - topLeft, - arcSize, - strokeWidth - ) + if (progressAngle > 0f) { + if (isComplete) { + drawArc( + brush = gradientBrush, + startAngle = startAngle, + sweepAngle = progressAngle, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + } else { + drawArc( + color = PRIMARY_YELLOW, + startAngle = startAngle, + sweepAngle = progressAngle, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + } + } // Progress arc circle val angle = Math.toRadians((startAngle + progressAngle).toDouble()) @@ -134,7 +154,29 @@ private fun ProgressArc( val y = arcCenterY + (radius * sin(angle)).toFloat() // Outer circle - drawArcSliderOuterCircle(workoutsCompleted, workoutGoal, gradientBrush, dotRadius, x, y) + when { + isComplete -> { + drawCircle( + brush = gradientBrush, + radius = dotRadius, + center = Offset(x, y) + ) + } + isZero -> { + drawCircle( + color = GRAY03, + radius = dotRadius, + center = Offset(x, y) + ) + } + else -> { + drawCircle( + color = PRIMARY_YELLOW, + radius = dotRadius, + center = Offset(x, y) + ) + } + } // Inner circle drawCircle( @@ -215,7 +257,7 @@ private fun DrawScope.drawArcSliderOuterCircle( } @Composable -private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int) { +private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int, isComplete: Boolean) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), @@ -225,7 +267,7 @@ private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int) verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - WorkoutsCompletedText(workoutsCompleted, workoutGoal) + WorkoutsCompletedText(workoutsCompleted, isComplete) Text( text = "/ $workoutGoal", @@ -237,7 +279,7 @@ private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int) ) } Text( - text = "Workouts this week", + text = "Days this week", fontSize = 14.sp, color = GRAY04, fontFamily = montserratFamily @@ -246,8 +288,8 @@ private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int) } @Composable -private fun WorkoutsCompletedText(workoutsCompleted: Int, workoutGoal: Int) { - if (workoutsCompleted == workoutGoal) { +private fun WorkoutsCompletedText(workoutsCompleted: Int, isComplete: Boolean) { + if (isComplete) { Text( text = "$workoutsCompleted", fontSize = 64.sp, diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt new file mode 100644 index 0000000..43854b8 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt @@ -0,0 +1,41 @@ +package com.cornellappdev.uplift.ui.components.profile.workouts + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +private fun WorkoutsSectionContent( + workoutsCompleted: Int, + workoutGoal: Int, + daysOfMonth: List, + completedDays: List, + reminderItems: List, + historyItems: List, + navigateToGoalsSection: () -> Unit, + navigateToRemindersSection: () -> Unit, + navigateToHistorySection: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + GoalsSection( + workoutsCompleted = workoutsCompleted, + workoutGoal = workoutGoal, + daysOfMonth = daysOfMonth, + completedDays = completedDays, + onClick = navigateToGoalsSection, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HistorySection( + historyItems = historyItems, + onClick = navigateToHistorySection, + modifier = Modifier.weight(1f) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index 695c299..86ec401 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -1,10 +1,15 @@ package com.cornellappdev.uplift.ui.screens.profile +import android.R.attr.name import android.annotation.SuppressLint +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsEndWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -15,6 +20,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -27,7 +34,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import com.cornellappdev.uplift.R +import com.cornellappdev.uplift.data.repositories.ProfileRepository +import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.components.general.UpliftTabRow import com.cornellappdev.uplift.ui.components.profile.workouts.GoalsSection import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem @@ -35,96 +46,63 @@ import com.cornellappdev.uplift.ui.components.profile.workouts.HistorySection import com.cornellappdev.uplift.ui.components.profile.workouts.MyRemindersSection import com.cornellappdev.uplift.ui.components.profile.ProfileHeaderSection import com.cornellappdev.uplift.ui.components.profile.workouts.ReminderItem +import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ProfileScreen() { - /* TODO: Replace with call to viewmodel */ - val name = "John Doe" - /* TODO: Replace with call to viewmodel */ - val gymDays = 132 - /* TODO: Replace with call to viewmodel */ - val streaks = 14 - /* TODO: Replace with call to viewmodel */ - val badges = 6 - /* TODO: Replace with call to viewmodel */ - val profilePicture = null - /* TODO: Replace with call to viewmodel */ - val workoutsCompleted = 3 - /* TODO: Replace with call to viewmodel */ - val workoutGoal = 5 - /* TODO: Replace with call to viewmodel */ - val daysOfMonth = (25..31).toList() - /* TODO: Replace with call to viewmodel */ - val completedDays = listOf(false, true, true, false, true, false, false) - /* TODO: Replace with call to viewmodel */ - val reminderItems = listOf( - ReminderItem("Mon", "8:00 AM", "9:00 AM", true), - ReminderItem("Tue", "8:00 AM", "12:00 PM", false), - ReminderItem("Wed", "8:00 AM", "9:00 AM", true), - ReminderItem("Thu", "8:00 AM", "9:00 AM", false), - ReminderItem("Fri", "11:30 AM", "12:00 PM", true), - ) - /* TODO: Replace with call to viewmodel */ - val historyItems = listOf( - HistoryItem("Morrison", "11:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Noyes", "1:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Teagle Up", "2:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Teagle Down", "12:00 PM", "Fri", "March 29, 2024"), - HistoryItem("Helen Newman", "10:00 AM", "Fri", "March 29, 2024"), - ) +fun ProfileScreen( + viewModel: ProfileViewModel = hiltViewModel(), + toSettings:() -> Unit, + toGoals:() -> Unit, + toHistory:() -> Unit +) { +// var tabIndex by remember { mutableIntStateOf(0) } +// val tabs = listOf("WORKOUTS", "ACHIEVEMENTS") - var tabIndex by remember { mutableIntStateOf(0) } - val tabs = listOf("WORKOUTS", "ACHIEVEMENTS") + val uiState by viewModel.uiStateFlow.collectAsState() - val scrollState = rememberScrollState() + LaunchedEffect(Unit) { + viewModel.reload() + } Scaffold( containerColor = Color.White, topBar = { - /* TODO: Replace {} with viewmodel nav call */ - ProfileScreenTopBar(navigateToSettings = {}) + ProfileScreenTopBar(navigateToSettings = toSettings) } ) { innerPadding -> Column( - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxSize() - .verticalScroll(scrollState) .padding( top = innerPadding.calculateTopPadding() + 24.dp, start = 16.dp, end = 16.dp, ) ) { - /* TODO: Replace {} with viewmodel function call */ ProfileHeaderSection( - name = name, - gymDays = gymDays, - streaks = streaks, - badges = badges, - profilePictureUri = profilePicture, - onPhotoSelected = {} + name = uiState.name, + gymDays = uiState.totalGymDays, + streaks = uiState.activeStreak, + profilePictureUri = uiState.profileImage?.let { Uri.parse(it) }, + onPhotoSelected = {}, + netID = uiState.netId + ) + WorkoutsSectionContent( + workoutsCompleted = uiState.workoutsCompleted, + workoutGoal = uiState.workoutGoal, + daysOfMonth = uiState.daysOfMonth, + completedDays = uiState.completedDays, + reminderItems= emptyList(), //implement + historyItems = uiState.historyItems, + navigateToGoalsSection = toGoals, + navigateToRemindersSection = { /* TODO: Replace {} with viewmodel nav call */ }, + navigateToHistorySection = toHistory ) - UpliftTabRow(tabIndex, tabs, onTabChange = { tabIndex = it }) - when (tabIndex) { - 0 -> WorkoutsSectionContent( - workoutsCompleted, - workoutGoal, - daysOfMonth, - completedDays, - reminderItems, - historyItems, - navigateToGoalsSection = { /* TODO: Replace {} with viewmodel nav call */ }, - navigateToRemindersSection = { /* TODO: Replace {} with viewmodel nav call */ }, - navigateToHistorySection = { /* TODO: Replace {} with viewmodel nav call */ } - ) - - 1 -> AchievementsSectionContent() - } } } @@ -142,21 +120,24 @@ private fun WorkoutsSectionContent( navigateToRemindersSection: () -> Unit, navigateToHistorySection: () -> Unit ) { - GoalsSection( - workoutsCompleted = workoutsCompleted, - workoutGoal = workoutGoal, - daysOfMonth = daysOfMonth, - completedDays = completedDays, - onClick = navigateToGoalsSection, - ) - MyRemindersSection( - reminderItems, - onClickHeader = navigateToRemindersSection, - ) - HistorySection( - historyItems = historyItems, - onClick = navigateToHistorySection, - ) + Column( + modifier = Modifier.fillMaxSize() + ) { + GoalsSection( + workoutsCompleted = workoutsCompleted, + workoutGoal = workoutGoal, + daysOfMonth = daysOfMonth, + completedDays = completedDays, + onClick = navigateToGoalsSection, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HistorySection( + historyItems = historyItems, + onClick = navigateToHistorySection, + modifier = Modifier.weight(1f) + ) + } } //TODO: Implement AchievementsSection @@ -196,9 +177,3 @@ private fun ProfileScreenTopBar( } ) } - -@Preview(showBackground = true) -@Composable -private fun ProfileScreenPreview() { - ProfileScreen() -} \ No newline at end of file From 200d3976011b3229007f39c4791940fe2daecc0c Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Sun, 8 Mar 2026 00:56:17 -0500 Subject: [PATCH 03/11] Implemented ProfileRepo and updated User model and repo # Conflicts: # app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt --- .../uplift/data/models/UserInfo.kt | 8 ++ .../data/repositories/ProfileRepository.kt | 129 ++++++++++++++++++ .../data/repositories/UserInfoRepository.kt | 25 ++++ 3 files changed, 162 insertions(+) create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt b/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt index d10aa9e..ca678b7 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt @@ -1,10 +1,18 @@ package com.cornellappdev.uplift.data.models +import com.cornellappdev.uplift.type.DateTime import kotlinx.serialization.Serializable + @Serializable data class UserInfo( val id: String, val email: String, val name: String, val netId: String, + val encodedImage: String?, + val activeStreak: Int?, + val maxStreak: Int?, + val streakStart: String?, + val workoutGoal: Int?, + val totalGymDays: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt new file mode 100644 index 0000000..1471ed3 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt @@ -0,0 +1,129 @@ +package com.cornellappdev.uplift.data.repositories + +import android.util.Log +import com.apollographql.apollo.ApolloClient +import com.cornellappdev.uplift.GetUserByNetIdQuery +import com.cornellappdev.uplift.GetWeeklyWorkoutDaysQuery +import com.cornellappdev.uplift.GetWorkoutsByIdQuery +import com.cornellappdev.uplift.SetWorkoutGoalsMutation +import java.time.Instant +import javax.inject.Inject +import javax.inject.Singleton + +data class ProfileData( + val name: String, + val netId: String, + val encodedImage: String?, + val totalGymDays: Int, + val activeStreak: Int, + val maxStreak: Int, + val streakStart: String?, + val workoutGoal: Int, + val workouts: List, + val weeklyWorkoutDays: List +) + +data class WorkoutDomain( + val gymName: String, + val timestamp: Long +) + +@Singleton +class ProfileRepository @Inject constructor( + private val userInfoRepository: UserInfoRepository, + private val apolloClient: ApolloClient +) { + suspend fun getProfile(): Result { + return try{ + val netId = userInfoRepository.getNetIdFromDataStore() + ?: return Result.failure(Exception("NetId missing")) + + val userResponse = apolloClient.query( + GetUserByNetIdQuery(netId) + ).execute() + + if (userResponse.hasErrors()) { + Log.e("ProfileRepo", "User query errors: ${userResponse.errors}") + return Result.failure(IllegalStateException("User query failed")) + } + + val user = userResponse.data?.getUserByNetId?.firstOrNull()?.userFields + ?: return Result.failure(IllegalStateException("User not found")) + + val userId = user.id.toIntOrNull() + ?: return Result.failure(IllegalStateException("Invalid user ID: ${user.id}")) + + val workoutResponse = apolloClient + .query(GetWorkoutsByIdQuery(userId)) + .execute() + + if (workoutResponse.hasErrors()) { + Log.e("ProfileRepo", "Workout query errors: ${workoutResponse.errors}") + } + + val workouts = if (workoutResponse.hasErrors()) { + emptyList() + } else { + workoutResponse.data?.getWorkoutsById?.filterNotNull() ?: emptyList() + } + + val workoutDomain = workouts.map { + WorkoutDomain( + gymName = it.workoutFields.gymName, + timestamp = Instant.parse(it.workoutFields.workoutTime.toString()).toEpochMilli() + ) + } + + val weeklyResponse = apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute() + if (weeklyResponse.hasErrors()) { + Log.e("ProfileRepo", "Weekly query errors=${weeklyResponse.errors}") + } + + val weeklyDays = if (weeklyResponse.hasErrors()) { + emptyList() + } else { + weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull() ?: emptyList() + } + + Result.success( + ProfileData( + name = user.name, + netId = user.netId, + encodedImage = user.encodedImage, + totalGymDays = user.totalGymDays, + activeStreak = user.activeStreak ?: 0, + maxStreak = user.maxStreak ?: 0, + streakStart = user.streakStart?.toString(), + workoutGoal = user.workoutGoal ?: 0, + workouts = workoutDomain, + weeklyWorkoutDays = weeklyDays + ) + ) + } catch (e: Exception) { + Log.e("ProfileRepo", "Failed to load profile", e) + Result.failure(e) + } + } + + suspend fun setWorkoutGoal(userId: Int, goal: Int): Result { + return try { + val response = apolloClient + .mutation( + SetWorkoutGoalsMutation( + id = userId, + workoutGoal = goal + ) + ) + .execute() + + if (response.hasErrors()) { + Result.failure(Exception("Goal update failed")) + } else { + Result.success(Unit) + } + + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index b674864..bfebf9d 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -14,6 +14,7 @@ import com.cornellappdev.uplift.SetWorkoutGoalsMutation import kotlinx.coroutines.flow.map; import kotlinx.coroutines.flow.firstOrNull import com.cornellappdev.uplift.data.models.UserInfo +import com.cornellappdev.uplift.type.DayOfWeekEnum import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider @@ -107,6 +108,24 @@ class UserInfoRepository @Inject constructor( } } + suspend fun syncUserToDataStore(netId: String): Boolean { + return try { + val user = getUserByNetId(netId) ?: return false + + storeId(user.id) + storeNetId(user.netId) + storeUsername(user.name) + storeEmail(user.email) + storeSkip(false) + + Log.d("UserInfoRepositoryImpl", "Synced existing user to DataStore: ${user.id}") + true + } catch (e: Exception) { + Log.e("UserInfoRepositoryImpl", "Error syncing user to DataStore", e) + false + } + } + suspend fun getUserByNetId(netId: String): UserInfo? { try { val response = apolloClient.query( @@ -120,6 +139,12 @@ class UserInfoRepository @Inject constructor( name = user.name, email = user.email ?: "", netId = user.netId, + encodedImage = user.encodedImage, + activeStreak = user.activeStreak, + maxStreak = user.maxStreak, + workoutGoal = user.workoutGoal, + streakStart = user.streakStart?.toString(), + totalGymDays = user.totalGymDays ) } catch (e: Exception) { Log.e("UserInfoRepositoryImpl", "Error getting user by netId: $e") From fc3d451b1f86c736b08e929c9dc6647a7fb82fbc Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Sun, 8 Mar 2026 00:58:17 -0500 Subject: [PATCH 04/11] Implemented Profile VM, updates Login VM and network # Conflicts: # app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt --- .../uplift/ui/MainNavigationWrapper.kt | 14 +- .../viewmodels/onboarding/LoginViewModel.kt | 14 +- .../ui/viewmodels/profile/ProfileViewModel.kt | 135 ++++++++++++++++++ .../cornellappdev/uplift/util/Functions.kt | 35 +++++ app/src/main/res/drawable/ic_bag.png | Bin 0 -> 2564 bytes 5 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt create mode 100644 app/src/main/res/drawable/ic_bag.png diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index 5373026..f4cab50 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -55,6 +55,7 @@ import com.cornellappdev.uplift.ui.viewmodels.nav.RootNavigationViewModel import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInViewModel import com.cornellappdev.uplift.util.ONBOARDING_FLAG import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel +import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel import com.cornellappdev.uplift.util.CHECK_IN_FLAG import com.cornellappdev.uplift.util.PRIMARY_BLACK import com.cornellappdev.uplift.util.PRIMARY_YELLOW @@ -77,14 +78,23 @@ fun MainNavigationWrapper( classDetailViewModel: ClassDetailViewModel = hiltViewModel(), rootNavigationViewModel: RootNavigationViewModel = hiltViewModel(), - ) { +) { + val confettiViewModel: ConfettiViewModel = hiltViewModel() + val checkInViewModel: CheckInViewModel = hiltViewModel() val rootNavigationUiState = rootNavigationViewModel.collectUiStateValue() val startDestination = rootNavigationUiState.startDestination val navController = rememberNavController() val systemUiController: SystemUiController = rememberSystemUiController() + val checkInUiState = checkInViewModel.collectUiStateValue() + val confettiUiState = confettiViewModel.collectUiStateValue() + + val profileViewModel: ProfileViewModel = hiltViewModel() + + + val yourShimmerTheme = defaultShimmerTheme.copy( shaderColors = listOf( Color.Unspecified.copy(alpha = 1f), @@ -244,7 +254,7 @@ fun MainNavigationWrapper( CapacityReminderScreen() } composable { - ProfileScreen() + ProfileScreen(profileViewModel, {}, {}, {}) } composable { MainReminderScreen() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt index aa0b3dc..bcb7bab 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt @@ -42,14 +42,20 @@ class LoginViewModel @Inject constructor( return@launch } when { - userInfoRepository.hasUser(netId) -> rootNavigationRepository.navigate( - UpliftRootRoute.Home - ) + userInfoRepository.hasUser(netId) -> { + val synced = userInfoRepository.syncUserToDataStore(netId) + if (synced) { + rootNavigationRepository.navigate(UpliftRootRoute.Home) + } else { + Log.e("Error", "Failed to sync existing user") + userInfoRepository.signOut() + } + } userInfoRepository.hasFirebaseUser() -> rootNavigationRepository.navigate( UpliftRootRoute.ProfileCreation ) - //TODO: Handle error + else -> { Log.e("Error", "Unexpected credential") userInfoRepository.signOut() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt new file mode 100644 index 0000000..8d24118 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -0,0 +1,135 @@ +package com.cornellappdev.uplift.ui.viewmodels.profile + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.cornellappdev.uplift.data.repositories.ProfileRepository +import com.cornellappdev.uplift.data.repositories.UpliftApiRepository +import com.cornellappdev.uplift.data.repositories.UserInfoRepository +import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem +import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import okhttp3.internal.format +import java.time.DayOfWeek +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale +import javax.inject.Inject + +data class ProfileUiState( + val loading: Boolean = false, + val error: Boolean = false, + val name: String = "", + val netId: String = "", + val profileImage: String? = null, + val totalGymDays: Int = 0, + val activeStreak: Int = 0, + val maxStreak: Int = 0, + val streakStart: String? = null, + val workoutGoal: Int = 0, + val historyItems: List = emptyList(), + val daysOfMonth: List = emptyList(), + val completedDays: List = emptyList(), + val workoutsCompleted: Int = 0 +) + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val userInfoRepository: UserInfoRepository, +) : UpliftViewModel(ProfileUiState()) { + + private var loadingJob: Job? = null + + fun reload() { + if (loadingJob?.isActive == true) return + loadingJob = loadProfile() + + } + + + private fun loadProfile(): Job = viewModelScope.launch { + applyMutation { copy(loading = true, error = false) } + + val result = profileRepository.getProfile() + + if (result.isSuccess) { + val profile = result.getOrNull()!! + + val historyItems = profile.workouts.map { + HistoryItem( + gymName = it.gymName, + time = formatTime.format( + Instant.ofEpochMilli(it.timestamp) + ), + date = formatDate.format( + Instant.ofEpochMilli(it.timestamp) + ), + timestamp = it.timestamp + ) + } + + val now = LocalDate.now() + val startOfWeek = now.with(DayOfWeek.MONDAY) + + val weekDates = (0..6).map { + startOfWeek.plusDays(it.toLong()) + } + + val daysOfMonth = weekDates.map { it.dayOfMonth } + + val completedDays = weekDates.map { date -> + profile.weeklyWorkoutDays.contains(date.toString()) + } + + val workoutsCompleted = profile.weeklyWorkoutDays.size + + applyMutation { + copy( + loading = false, + name = profile.name, + netId = profile.netId, + profileImage = profile.encodedImage, + totalGymDays = profile.totalGymDays, + activeStreak = profile.activeStreak, + maxStreak = profile.maxStreak, + streakStart = profile.streakStart, + workoutGoal = profile.workoutGoal, + historyItems = historyItems, + daysOfMonth = daysOfMonth, + completedDays = completedDays, + workoutsCompleted = workoutsCompleted + ) + } + } else { + Log.e("profile VM", "Failed to load profile", result.exceptionOrNull()) + applyMutation { copy(loading = false, error = true) } + } + + } + + + fun updateWorkoutGoal(goal: Int) = viewModelScope.launch { + val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return@launch + val result = profileRepository.setWorkoutGoal(userId, goal) + + if (result.isSuccess) { + reload() + } else { + Log.e("profile VM", "Failed to update workout goal", result.exceptionOrNull()) + } + } + + private val formatTime = DateTimeFormatter + .ofPattern("h:mm a") + .withLocale(Locale.US) + .withZone(ZoneId.systemDefault()) + + private val formatDate = DateTimeFormatter + .ofPattern("MMMM d, yyyy • h:mm a") + .withLocale(Locale.US) + .withZone(ZoneId.systemDefault()) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt index d3cf75a..0988bc5 100644 --- a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt +++ b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt @@ -183,3 +183,38 @@ val startTimeComparator = { class1: UpliftClass, class2: UpliftClass -> class1.time.end.compareTo(class2.time.end) } } + +/** + * Returns a relative time string such as: + * "1 day ago", "2 weeks ago", "1 month ago", "1 year ago" + */ +fun Calendar.timeAgoString(): String { + val now = Calendar.getInstance() + + val diffMillis = now.timeInMillis - this.timeInMillis + if (diffMillis < 0) return "Today" + + val diffDays = diffMillis / (1000 * 60 * 60 * 24) + + val diffWeeks = diffDays / 7 + val diffMonths = diffDays / 30 + val diffYears = diffDays / 365 + + return when { + diffDays < 1 -> "Today" + + diffDays == 1L -> "1 day ago" + diffDays in 2..6 -> "$diffDays days ago" + + diffWeeks == 1L -> "1 week ago" + diffWeeks in 2..4 -> "$diffWeeks weeks ago" + + diffMonths == 1L -> "1 month ago" + diffMonths in 2..11 -> "$diffMonths months ago" + + diffYears == 1L -> "1 year ago" + diffYears > 1L -> "$diffYears years ago" + + else -> "Today" + } +} diff --git a/app/src/main/res/drawable/ic_bag.png b/app/src/main/res/drawable/ic_bag.png new file mode 100644 index 0000000000000000000000000000000000000000..41b1efe9c23f3c93f1ef8a6a07c9a6c3d9de8921 GIT binary patch literal 2564 zcmV+f3j6hmP)?KiCDJ+(qC=NHH8 z2?W++D_pcz{P+%R-Fy!$xj_ibo1p*GJm|af2=bw{%5t#L?RL*VhLAB5kbrt5pmkr( zBXcf~g$|$Vo&1A4*FaZt|d5{4Zt^|4z zlVEE3!yEsCd}}b=-v8-3_}!bE;PaupksCMHSY7BNR1gtJP|n2dZ0qpwa0eI+!sOXUp|2BXcO_s@@@ z^UEGV0*-gOJ4etv|MBbxHjEO>xAWAK^H720nHU@cgJG-^7&}JMpa|uy!iV;MzO4{L zA#ehyZAo5w$A_elf6(Gk-2_-HvcyBHfiR4O@^aVf_Vr6*rN-6?=Pwj;6@2`0PpjI- za{^gz?C8voPy^)@moG=-q7yb^s*pS!O;uwZhN zS!nUni__>Rmqg4mUk8I=2niGtNd>MrDj;W=C^g1Z`Z)Ou3Y{uQ4ctkFLZ%^V(47RH z7%^7AFHTNCDj;W==-l33Bu8QIhsat6k%^AdYLpsuC*eV21c_9jI>SWBQcO)mm{tu0 zBh{cQ3CUDIf5}v!f|C?S4HPnqFzYlz4Z4ybfFP|#1u7D$VQ=*`R|Dt_ea(ULf+!VG zL`YQyDmYmT+%&z`qH@##$jwP^7?O%M1~r#m*gbR0S#3(?RM+0_Oj_qJ04ofCUmv z1{d351)wh}Rc=;}hWZdL)m>dpHmX;N-1>inYg)jFD8X@r@+#$`CWR;D}02 zX9?8R)qxWU(Bkx|_u$#>{)o1rFD>A3IH0P!3RVSl45!|`0*!~3nU5q7%`YHDjD z4j&vCgs!gchVJg}hE&N&>Jz9!Y!b}>aF&)x&CPE^^NHpd0tN>M`L{#78_|C4tF_>C zI$^aaD=XtAc~X@YWPV<+H|BV{zoWlC0_VjH-*FV!EU$1nX_l$z`^|@ z6NHv>$(1m-h)V(ykZZ9t(db9ZEgIwFLL_`b;RLa8AR})Dp(dcVIlC(ud1@(p?h|)Cg|~cFhbel_K}^ntR5m_YH}L7KXSL=b4G;G2oiWj$Zi+w>JLJypa&}| zT#9HTC6FygWBc92h<^6!K#d`L2zHTllw1MAe=bz}q~6>lsowbcTHb zsenQnv7QRZA^zpcC7!}k4M7Wv;d8V91&_xQa~_QznNOlWd$eic15(2D?Dn~sz9>zf zo&Fo&pA4i34E21w_=zpxctQkR8x0p{!b+eC(C(^ANC&il+IVOz?!I{PT@4N$oQO%w z2{cvz!kheDBB0QUCN(w6e?L|FB$-7$ZgL{H64VJ$s?P9XM4@%rl%C$nSN*M(QYF14 z5+XsCp_IINJZJW80jVL|9Euieb912JQ7Z%mMWWvie1amT>I)ubSk`_oyuI-U^t_5l z+nfY61*gNY6~67z<_!gp=3`BQiL@PZj51hPlHK_O0z{X;R8Z=yC#b??u>@nb;toR}O-+HbsU zlcw-z&&6JcB2Zw#iqrMf1~6xhCA>|30gFN+ZJRM^;2ttvB~T=xn?11qApEv*G$MS_ z%U3V%se|2ByCL#noT!}Pm~{y)`Z4L!l0d$SX0fh{O8EH!AC&xPxrBG(y$xW_{R!Yq z7gB-N95!wWMGKxJn$2b_jcO($L+wv%c*%{H;NzjAvw*Kxrjxd(18sz$=s;>PnX*gG zYIw{9=sK_BsGUA_nwt#IHc>$&YbweT0Y{G>0V5+-&YE1fzZ5FZ6%<%ubaY%aQ<%tP zeo%`!ei_au-g@&bG@GGtJK246b6rZ)_tij6p9y3VqoZS@`Tr3fRXZn8J;0J@$ylPc z7M4f|ISl^p|P~I{UbkFHrXY$T+p0Y5%KwmRvq++1$!bR!nXLi6> zH$R7TFm*feLJ!^8zbC$7j;jNR?3G^U+Q4=EWbJ|wWQ|lMI(_C{_%5OAqs#F3{sI2p zgt^DvGf*y7zVhM4&X((6{F7H*QpLi2s7w}}*VDY=+Tiu1N?8f0gr|KOoVIVzjX~Fk zmmwWsK?ShRIHSmN>oGM0ZyIWX4xEm aedvEA2;ig&CBxnT0000 Date: Sun, 8 Mar 2026 00:59:04 -0500 Subject: [PATCH 05/11] tested logworkout call for profile page testing, WIP --- .../uplift/data/repositories/CheckInRepository.kt | 10 +++++++++- .../uplift/ui/viewmodels/profile/CheckInViewModel.kt | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt index d0384d1..1e8b165 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt @@ -150,7 +150,14 @@ class CheckInRepository @Inject constructor( * Logs a completed workout to the backend. Returns true if the mutation succeeded, false otherwise. */ suspend fun logWorkoutFromCheckIn(gymId: Int): Boolean { - val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return false + val userIdString = userInfoRepository.getUserIdFromDataStore() + val userId = userIdString?.toIntOrNull() + + if (userId == null) { + Log.e("CheckInRepository", "Missing or invalid userId in DataStore: $userIdString") + return false + } + val time = Instant.now().toString() return try { @@ -161,6 +168,7 @@ class CheckInRepository @Inject constructor( val ok = response.data?.logWorkout?.workoutFields != null && !response.hasErrors() if (!ok) { Log.e("CheckInRepository", "LogWorkout errors=${response.errors}") + Log.e("CheckInRepository", "LogWorkout response data=${response.data}") } ok } catch (t: Throwable){ diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt index 09c0516..0a17dac 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt @@ -150,7 +150,12 @@ class CheckInViewModel @Inject constructor( ) } confettiRepository.showConfetti(ConfettiViewModel.ConfettiUiState()) - checkInRepository.logWorkoutFromCheckIn(gymIdInt) + val logged = checkInRepository.logWorkoutFromCheckIn(gymIdInt) + if (logged) { + Log.d(tag, "Workout successfully logged to backend") + } else { + Log.e(tag, "Workout failed to log to backend") + } } catch (e: Exception) { Log.e(tag, "Error checking in", e) } From 07f6cab0301e5af51c9d210c94194f34508935ab Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Mon, 9 Mar 2026 14:56:49 -0400 Subject: [PATCH 06/11] Rebased and Addressed Copilot Comments --- .../uplift/data/models/UserInfo.kt | 1 - .../data/repositories/CheckInRepository.kt | 1 - .../data/repositories/ProfileRepository.kt | 4 +- .../data/repositories/UserInfoRepository.kt | 1 - .../uplift/ui/MainNavigationWrapper.kt | 13 +----- .../profile/ProfileHeaderSection.kt | 19 ++------- .../profile/workouts/HistorySection.kt | 3 -- .../profile/workouts/WorkoutSection.kt | 41 ------------------- .../ui/screens/profile/ProfileScreen.kt | 15 +------ .../ui/viewmodels/profile/ProfileViewModel.kt | 4 +- 10 files changed, 9 insertions(+), 93 deletions(-) delete mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt diff --git a/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt b/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt index ca678b7..c472092 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt @@ -1,5 +1,4 @@ package com.cornellappdev.uplift.data.models -import com.cornellappdev.uplift.type.DateTime import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt index 1e8b165..52e380d 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt @@ -168,7 +168,6 @@ class CheckInRepository @Inject constructor( val ok = response.data?.logWorkout?.workoutFields != null && !response.hasErrors() if (!ok) { Log.e("CheckInRepository", "LogWorkout errors=${response.errors}") - Log.e("CheckInRepository", "LogWorkout response data=${response.data}") } ok } catch (t: Throwable){ diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt index 1471ed3..0e5818c 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt @@ -91,8 +91,8 @@ class ProfileRepository @Inject constructor( netId = user.netId, encodedImage = user.encodedImage, totalGymDays = user.totalGymDays, - activeStreak = user.activeStreak ?: 0, - maxStreak = user.maxStreak ?: 0, + activeStreak = user.activeStreak, + maxStreak = user.maxStreak, streakStart = user.streakStart?.toString(), workoutGoal = user.workoutGoal ?: 0, workouts = workoutDomain, diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index bfebf9d..345feb4 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -14,7 +14,6 @@ import com.cornellappdev.uplift.SetWorkoutGoalsMutation import kotlinx.coroutines.flow.map; import kotlinx.coroutines.flow.firstOrNull import com.cornellappdev.uplift.data.models.UserInfo -import com.cornellappdev.uplift.type.DayOfWeekEnum import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index f4cab50..d99ae49 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -53,9 +53,7 @@ import com.cornellappdev.uplift.ui.viewmodels.classes.ClassDetailViewModel import com.cornellappdev.uplift.ui.viewmodels.gyms.GymDetailViewModel import com.cornellappdev.uplift.ui.viewmodels.nav.RootNavigationViewModel import com.cornellappdev.uplift.ui.viewmodels.profile.CheckInViewModel -import com.cornellappdev.uplift.util.ONBOARDING_FLAG import com.cornellappdev.uplift.ui.viewmodels.profile.ConfettiViewModel -import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel import com.cornellappdev.uplift.util.CHECK_IN_FLAG import com.cornellappdev.uplift.util.PRIMARY_BLACK import com.cornellappdev.uplift.util.PRIMARY_YELLOW @@ -88,13 +86,6 @@ fun MainNavigationWrapper( val navController = rememberNavController() val systemUiController: SystemUiController = rememberSystemUiController() - val checkInUiState = checkInViewModel.collectUiStateValue() - val confettiUiState = confettiViewModel.collectUiStateValue() - - val profileViewModel: ProfileViewModel = hiltViewModel() - - - val yourShimmerTheme = defaultShimmerTheme.copy( shaderColors = listOf( Color.Unspecified.copy(alpha = 1f), @@ -107,7 +98,7 @@ fun MainNavigationWrapper( val items = listOfNotNull( BottomNavScreens.Home, BottomNavScreens.Classes, - BottomNavScreens.Profile.takeIf { ONBOARDING_FLAG } + BottomNavScreens.Profile ) systemUiController.setStatusBarColor(PRIMARY_YELLOW) @@ -254,7 +245,7 @@ fun MainNavigationWrapper( CapacityReminderScreen() } composable { - ProfileScreen(profileViewModel, {}, {}, {}) + ProfileScreen(toSettings = {}, toGoals = {}, toHistory = {}) } composable { MainReminderScreen() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt index 2fa3f8f..9f2b801 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt @@ -1,36 +1,23 @@ package com.cornellappdev.uplift.ui.components.profile -import android.R.attr.fontFamily -import android.R.attr.fontWeight import android.net.Uri -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.cornellappdev.uplift.R import com.cornellappdev.uplift.ui.components.onboarding.PhotoPicker import com.cornellappdev.uplift.ui.components.onboarding.ScreenType -import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.GRAY04 import com.cornellappdev.uplift.util.montserratFamily @@ -39,7 +26,7 @@ fun ProfileHeaderSection( name: String, gymDays: Int, streaks: Int, - netID: String, + netId: String, profilePictureUri: Uri?, onPhotoSelected: (Uri) -> Unit ){ @@ -53,7 +40,7 @@ fun ProfileHeaderSection( onPhotoSelected = onPhotoSelected, screenType = ScreenType.PROFILE ) - ProfileHeaderInfoDisplay(name, gymDays, streaks, netID, modifier = Modifier.weight(1f)) + ProfileHeaderInfoDisplay(name, gymDays, streaks, netId, modifier = Modifier.weight(1f)) } } @@ -146,6 +133,6 @@ private fun ProfileHeaderSectionPreview() { streaks = 15, profilePictureUri = null, onPhotoSelected = {}, - netID = "mv477" + netId = "mv477" ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt index e97b7d1..067480b 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt @@ -13,15 +13,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt deleted file mode 100644 index 43854b8..0000000 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.cornellappdev.uplift.ui.components.profile.workouts - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -private fun WorkoutsSectionContent( - workoutsCompleted: Int, - workoutGoal: Int, - daysOfMonth: List, - completedDays: List, - reminderItems: List, - historyItems: List, - navigateToGoalsSection: () -> Unit, - navigateToRemindersSection: () -> Unit, - navigateToHistorySection: () -> Unit -) { - Column( - modifier = Modifier.fillMaxSize() - ) { - GoalsSection( - workoutsCompleted = workoutsCompleted, - workoutGoal = workoutGoal, - daysOfMonth = daysOfMonth, - completedDays = completedDays, - onClick = navigateToGoalsSection, - ) - Spacer(modifier = Modifier.height(24.dp)) - - HistorySection( - historyItems = historyItems, - onClick = navigateToHistorySection, - modifier = Modifier.weight(1f) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index 86ec401..156f980 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -1,6 +1,5 @@ package com.cornellappdev.uplift.ui.screens.profile -import android.R.attr.name import android.annotation.SuppressLint import android.net.Uri import androidx.compose.foundation.layout.Arrangement @@ -9,9 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsEndWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -23,27 +19,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import com.cornellappdev.uplift.R -import com.cornellappdev.uplift.data.repositories.ProfileRepository -import com.cornellappdev.uplift.data.repositories.UserInfoRepository -import com.cornellappdev.uplift.ui.components.general.UpliftTabRow import com.cornellappdev.uplift.ui.components.profile.workouts.GoalsSection import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.components.profile.workouts.HistorySection -import com.cornellappdev.uplift.ui.components.profile.workouts.MyRemindersSection import com.cornellappdev.uplift.ui.components.profile.ProfileHeaderSection import com.cornellappdev.uplift.ui.components.profile.workouts.ReminderItem import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel @@ -90,7 +77,7 @@ fun ProfileScreen( streaks = uiState.activeStreak, profilePictureUri = uiState.profileImage?.let { Uri.parse(it) }, onPhotoSelected = {}, - netID = uiState.netId + netId = uiState.netId ) WorkoutsSectionContent( workoutsCompleted = uiState.workoutsCompleted, diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 8d24118..72451fa 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -3,14 +3,12 @@ package com.cornellappdev.uplift.ui.viewmodels.profile import android.util.Log import androidx.lifecycle.viewModelScope import com.cornellappdev.uplift.data.repositories.ProfileRepository -import com.cornellappdev.uplift.data.repositories.UpliftApiRepository import com.cornellappdev.uplift.data.repositories.UserInfoRepository import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import okhttp3.internal.format import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate @@ -129,7 +127,7 @@ class ProfileViewModel @Inject constructor( .withZone(ZoneId.systemDefault()) private val formatDate = DateTimeFormatter - .ofPattern("MMMM d, yyyy • h:mm a") + .ofPattern("MMMM d, yyyy") .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) } \ No newline at end of file From 2af071e3703a3ead76133a1a70ce61ed9a45242c Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 12 Mar 2026 19:02:16 -0400 Subject: [PATCH 07/11] Adressing Comments --- app/src/main/graphql/schema.graphqls | 13 +- .../uplift/data/models/ProfileData.kt | 17 ++ .../uplift/data/models/WorkoutDomain.kt | 6 + .../data/repositories/ProfileRepository.kt | 151 ++++++++---------- .../uplift/ui/MainNavigationWrapper.kt | 5 +- .../profile/workouts/HistorySection.kt | 2 +- .../ui/screens/profile/ProfileScreen.kt | 54 +++++-- .../ui/viewmodels/profile/ProfileViewModel.kt | 127 ++++++++------- .../cornellappdev/uplift/util/Functions.kt | 6 +- app/src/main/res/drawable/ic_bag.png | Bin 2564 -> 0 bytes app/src/main/res/drawable/ic_dufflebag.xml | 41 +++++ 11 files changed, 255 insertions(+), 167 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/models/ProfileData.kt create mode 100644 app/src/main/java/com/cornellappdev/uplift/data/models/WorkoutDomain.kt delete mode 100644 app/src/main/res/drawable/ic_bag.png create mode 100644 app/src/main/res/drawable/ic_dufflebag.xml diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls index 2b3e050..6d88007 100644 --- a/app/src/main/graphql/schema.graphqls +++ b/app/src/main/graphql/schema.graphqls @@ -378,9 +378,11 @@ type User { totalGymDays: Int! """ - The start date of the most recent active streak, up until the current date. + The start datetime of the most recent active streak (midnight of the day in local timezone), up until the current date. """ - streakStart: Date + streakStart: DateTime + + workoutHistory: [Workout] } type Giveaway { @@ -421,13 +423,6 @@ type Friendship { friend: User } -""" -The `Date` scalar type represents a Date -value as specified by -[iso8601](https://en.wikipedia.org/wiki/ISO_8601). -""" -scalar Date - type Workout { id: ID! diff --git a/app/src/main/java/com/cornellappdev/uplift/data/models/ProfileData.kt b/app/src/main/java/com/cornellappdev/uplift/data/models/ProfileData.kt new file mode 100644 index 0000000..b9d5a90 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/models/ProfileData.kt @@ -0,0 +1,17 @@ +package com.cornellappdev.uplift.data.models + +import android.net.Uri + + +data class ProfileData( + val name: String, + val netId: String, + val encodedImage: String?, + val totalGymDays: Int, + val activeStreak: Int, + val maxStreak: Int, + val streakStart: String?, + val workoutGoal: Int, + val workouts: List, + val weeklyWorkoutDays: List +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/models/WorkoutDomain.kt b/app/src/main/java/com/cornellappdev/uplift/data/models/WorkoutDomain.kt new file mode 100644 index 0000000..5de644c --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/data/models/WorkoutDomain.kt @@ -0,0 +1,6 @@ +package com.cornellappdev.uplift.data.models + +data class WorkoutDomain( + val gymName: String, + val timestamp: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt index 0e5818c..0d15d82 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt @@ -6,124 +6,105 @@ import com.cornellappdev.uplift.GetUserByNetIdQuery import com.cornellappdev.uplift.GetWeeklyWorkoutDaysQuery import com.cornellappdev.uplift.GetWorkoutsByIdQuery import com.cornellappdev.uplift.SetWorkoutGoalsMutation +import com.cornellappdev.uplift.data.models.ProfileData +import com.cornellappdev.uplift.data.models.WorkoutDomain +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import java.time.Instant import javax.inject.Inject import javax.inject.Singleton -data class ProfileData( - val name: String, - val netId: String, - val encodedImage: String?, - val totalGymDays: Int, - val activeStreak: Int, - val maxStreak: Int, - val streakStart: String?, - val workoutGoal: Int, - val workouts: List, - val weeklyWorkoutDays: List -) - -data class WorkoutDomain( - val gymName: String, - val timestamp: Long -) @Singleton class ProfileRepository @Inject constructor( private val userInfoRepository: UserInfoRepository, private val apolloClient: ApolloClient ) { - suspend fun getProfile(): Result { - return try{ - val netId = userInfoRepository.getNetIdFromDataStore() - ?: return Result.failure(Exception("NetId missing")) - - val userResponse = apolloClient.query( - GetUserByNetIdQuery(netId) - ).execute() - - if (userResponse.hasErrors()) { - Log.e("ProfileRepo", "User query errors: ${userResponse.errors}") - return Result.failure(IllegalStateException("User query failed")) - } + suspend fun getProfile(): Result = runCatching { + val netId = userInfoRepository.getNetIdFromDataStore() + ?: throw IllegalStateException("NetId missing") + + val userResponse = apolloClient.query( + GetUserByNetIdQuery(netId) + ).execute() + + if (userResponse.hasErrors()) { + Log.e("ProfileRepo", "User query errors: ${userResponse.errors}") + throw IllegalStateException("User query failed") + } - val user = userResponse.data?.getUserByNetId?.firstOrNull()?.userFields - ?: return Result.failure(IllegalStateException("User not found")) + val user = userResponse.data?.getUserByNetId?.firstOrNull()?.userFields + ?: throw IllegalStateException("User not found") - val userId = user.id.toIntOrNull() - ?: return Result.failure(IllegalStateException("Invalid user ID: ${user.id}")) + val userId = user.id.toIntOrNull() + ?: throw IllegalStateException("Invalid user ID: ${user.id}") - val workoutResponse = apolloClient - .query(GetWorkoutsByIdQuery(userId)) - .execute() + coroutineScope { + val workoutDeferred = async { + apolloClient.query(GetWorkoutsByIdQuery(userId)).execute() + } + val weeklyDeferred = async { + apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute() + } + val workoutResponse = workoutDeferred.await() if (workoutResponse.hasErrors()) { Log.e("ProfileRepo", "Workout query errors: ${workoutResponse.errors}") + throw IllegalStateException("Workout query failed") } - val workouts = if (workoutResponse.hasErrors()) { - emptyList() - } else { - workoutResponse.data?.getWorkoutsById?.filterNotNull() ?: emptyList() - } + val workouts = workoutResponse.data?.getWorkoutsById?.filterNotNull().orEmpty() val workoutDomain = workouts.map { WorkoutDomain( gymName = it.workoutFields.gymName, - timestamp = Instant.parse(it.workoutFields.workoutTime.toString()).toEpochMilli() + timestamp = Instant.parse(it.workoutFields.workoutTime.toString()) + .toEpochMilli() ) } - val weeklyResponse = apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute() + val weeklyResponse = weeklyDeferred.await() if (weeklyResponse.hasErrors()) { - Log.e("ProfileRepo", "Weekly query errors=${weeklyResponse.errors}") - } - - val weeklyDays = if (weeklyResponse.hasErrors()) { - emptyList() - } else { - weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull() ?: emptyList() + Log.e("ProfileRepo", "Weekly query errors: ${weeklyResponse.errors}") + throw IllegalStateException("Weekly workout days query failed") } - Result.success( - ProfileData( - name = user.name, - netId = user.netId, - encodedImage = user.encodedImage, - totalGymDays = user.totalGymDays, - activeStreak = user.activeStreak, - maxStreak = user.maxStreak, - streakStart = user.streakStart?.toString(), - workoutGoal = user.workoutGoal ?: 0, - workouts = workoutDomain, - weeklyWorkoutDays = weeklyDays - ) + val weeklyDays = weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull().orEmpty() + + ProfileData( + name = user.name, + netId = user.netId, + encodedImage = user.encodedImage, + totalGymDays = user.totalGymDays, + activeStreak = user.activeStreak, + maxStreak = user.maxStreak, + streakStart = user.streakStart?.toString(), + workoutGoal = user.workoutGoal ?: 0, + workouts = workoutDomain, + weeklyWorkoutDays = weeklyDays ) - } catch (e: Exception) { - Log.e("ProfileRepo", "Failed to load profile", e) - Result.failure(e) } + }.onFailure { e -> + Log.e("ProfileRepo", "Failed to load profile", e) } - suspend fun setWorkoutGoal(userId: Int, goal: Int): Result { - return try { - val response = apolloClient - .mutation( - SetWorkoutGoalsMutation( - id = userId, - workoutGoal = goal - ) - ) - .execute() + suspend fun setWorkoutGoal(goal: Int): Result = runCatching { + val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() + ?: throw IllegalStateException("Missing user ID") - if (response.hasErrors()) { - Result.failure(Exception("Goal update failed")) - } else { - Result.success(Unit) - } + val response = apolloClient + .mutation( + SetWorkoutGoalsMutation( + id = userId, + workoutGoal = goal + ) + ) + .execute() - } catch (e: Exception) { - Result.failure(e) + if (response.hasErrors()) { + throw IllegalStateException("Goal update failed") } + }.onFailure { e -> + Log.e("ProfileRepo", "Failed to update workout goal", e) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index d99ae49..90a133e 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -77,9 +77,6 @@ fun MainNavigationWrapper( rootNavigationViewModel: RootNavigationViewModel = hiltViewModel(), ) { - - val confettiViewModel: ConfettiViewModel = hiltViewModel() - val checkInViewModel: CheckInViewModel = hiltViewModel() val rootNavigationUiState = rootNavigationViewModel.collectUiStateValue() val startDestination = rootNavigationUiState.startDestination @@ -245,7 +242,7 @@ fun MainNavigationWrapper( CapacityReminderScreen() } composable { - ProfileScreen(toSettings = {}, toGoals = {}, toHistory = {}) + ProfileScreen() } composable { MainReminderScreen() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt index 067480b..63b2505 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt @@ -135,7 +135,7 @@ private fun EmptyHistorySection(){ horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(id = R.drawable.ic_bag), + painter = painterResource(id = R.drawable.ic_dufflebag), contentDescription = null, modifier = Modifier .width(64.99967.dp) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index 156f980..a3b0918 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -24,15 +24,18 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import com.cornellappdev.uplift.R import com.cornellappdev.uplift.ui.components.profile.workouts.GoalsSection import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.components.profile.workouts.HistorySection import com.cornellappdev.uplift.ui.components.profile.ProfileHeaderSection import com.cornellappdev.uplift.ui.components.profile.workouts.ReminderItem +import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileUiState import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily @@ -41,20 +44,21 @@ import com.cornellappdev.uplift.util.montserratFamily @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProfileScreen( - viewModel: ProfileViewModel = hiltViewModel(), - toSettings:() -> Unit, - toGoals:() -> Unit, - toHistory:() -> Unit + viewModel: ProfileViewModel = hiltViewModel() ) { -// var tabIndex by remember { mutableIntStateOf(0) } -// val tabs = listOf("WORKOUTS", "ACHIEVEMENTS") - val uiState by viewModel.uiStateFlow.collectAsState() - LaunchedEffect(Unit) { - viewModel.reload() - } + ProfileScreenContent(uiState,viewModel::toSettings,viewModel::toGoals, viewModel::toHistory) + +} +@Composable +private fun ProfileScreenContent( + uiState: ProfileUiState, + toSettings: () -> Unit, + toGoals: () -> Unit, + toHistory: () -> Unit +) { Scaffold( containerColor = Color.White, topBar = { @@ -75,7 +79,7 @@ fun ProfileScreen( name = uiState.name, gymDays = uiState.totalGymDays, streaks = uiState.activeStreak, - profilePictureUri = uiState.profileImage?.let { Uri.parse(it) }, + profilePictureUri = uiState.profileImage, onPhotoSelected = {}, netId = uiState.netId ) @@ -164,3 +168,31 @@ private fun ProfileScreenTopBar( } ) } + +@Preview +@Composable +private fun ProfileScreenContentPreview() { + ProfileScreenContent( + uiState = ProfileUiState( + name = "Melissa Velasquez", + netId = "mv477", + totalGymDays = 42, + activeStreak = 5, + workoutGoal = 4, + historyItems = listOf( + HistoryItem( + gymName = "Teagle", + time = "10:00 AM", + date = "March 29, 2024", + timestamp = 0L + ) + ), + daysOfMonth = listOf(10, 11, 12, 13, 14, 15, 16), + completedDays = listOf(true, false, true, false, true, false, false), + workoutsCompleted = 3 + ), + {}, + {}, + {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 72451fa..9b1b3bf 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -1,10 +1,14 @@ package com.cornellappdev.uplift.ui.viewmodels.profile +import android.net.Uri import android.util.Log import androidx.lifecycle.viewModelScope +import coil.util.CoilUtils.result import com.cornellappdev.uplift.data.repositories.ProfileRepository import com.cornellappdev.uplift.data.repositories.UserInfoRepository +import com.cornellappdev.uplift.ui.UpliftRootRoute import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem +import com.cornellappdev.uplift.ui.nav.RootNavigationRepository import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -22,7 +26,7 @@ data class ProfileUiState( val error: Boolean = false, val name: String = "", val netId: String = "", - val profileImage: String? = null, + val profileImage: Uri? = null, val totalGymDays: Int = 0, val activeStreak: Int = 0, val maxStreak: Int = 0, @@ -37,11 +41,15 @@ data class ProfileUiState( @HiltViewModel class ProfileViewModel @Inject constructor( private val profileRepository: ProfileRepository, - private val userInfoRepository: UserInfoRepository, + private val rootNavigationRepository: RootNavigationRepository, ) : UpliftViewModel(ProfileUiState()) { private var loadingJob: Job? = null + init { + reload() + } + fun reload() { if (loadingJob?.isActive == true) return loadingJob = loadProfile() @@ -54,65 +62,62 @@ class ProfileViewModel @Inject constructor( val result = profileRepository.getProfile() - if (result.isSuccess) { - val profile = result.getOrNull()!! - - val historyItems = profile.workouts.map { - HistoryItem( - gymName = it.gymName, - time = formatTime.format( - Instant.ofEpochMilli(it.timestamp) - ), - date = formatDate.format( - Instant.ofEpochMilli(it.timestamp) - ), - timestamp = it.timestamp - ) - } - - val now = LocalDate.now() - val startOfWeek = now.with(DayOfWeek.MONDAY) - - val weekDates = (0..6).map { - startOfWeek.plusDays(it.toLong()) - } - - val daysOfMonth = weekDates.map { it.dayOfMonth } - - val completedDays = weekDates.map { date -> - profile.weeklyWorkoutDays.contains(date.toString()) - } - - val workoutsCompleted = profile.weeklyWorkoutDays.size - - applyMutation { - copy( - loading = false, - name = profile.name, - netId = profile.netId, - profileImage = profile.encodedImage, - totalGymDays = profile.totalGymDays, - activeStreak = profile.activeStreak, - maxStreak = profile.maxStreak, - streakStart = profile.streakStart, - workoutGoal = profile.workoutGoal, - historyItems = historyItems, - daysOfMonth = daysOfMonth, - completedDays = completedDays, - workoutsCompleted = workoutsCompleted - ) - } - } else { + val profile = result.getOrNull() + if (profile == null) { Log.e("profile VM", "Failed to load profile", result.exceptionOrNull()) applyMutation { copy(loading = false, error = true) } + return@launch + } + val historyItems = profile.workouts.map { + HistoryItem( + gymName = it.gymName, + time = formatTime.format( + Instant.ofEpochMilli(it.timestamp) + ), + date = formatDate.format( + Instant.ofEpochMilli(it.timestamp) + ), + timestamp = it.timestamp + ) } - } + val now = LocalDate.now() + val startOfWeek = now.with(DayOfWeek.MONDAY) + + val weekDates = (0..6).map { + startOfWeek.plusDays(it.toLong()) + } + + val daysOfMonth = weekDates.map { it.dayOfMonth } + + val completedDays = weekDates.map { date -> + profile.weeklyWorkoutDays.contains(date.toString()) + } + val workoutsCompleted = profile.weeklyWorkoutDays.size + + applyMutation { + copy( + loading = false, + name = profile.name, + netId = profile.netId, + profileImage = profile.encodedImage?.let(Uri::parse), + totalGymDays = profile.totalGymDays, + activeStreak = profile.activeStreak, + maxStreak = profile.maxStreak, + streakStart = profile.streakStart, + workoutGoal = profile.workoutGoal, + historyItems = historyItems, + daysOfMonth = daysOfMonth, + completedDays = completedDays, + workoutsCompleted = workoutsCompleted + ) + + } + } fun updateWorkoutGoal(goal: Int) = viewModelScope.launch { - val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return@launch - val result = profileRepository.setWorkoutGoal(userId, goal) + val result = profileRepository.setWorkoutGoal(goal) if (result.isSuccess) { reload() @@ -121,6 +126,20 @@ class ProfileViewModel @Inject constructor( } } + fun toSettings() { + rootNavigationRepository.navigate(UpliftRootRoute.Settings) + } + + fun toGoals() { + // replace with the actual route once goals exists + } + + fun toHistory() { + // replace with the actual route once history exists + } + + + private val formatTime = DateTimeFormatter .ofPattern("h:mm a") .withLocale(Locale.US) diff --git a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt index 0988bc5..516c1da 100644 --- a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt +++ b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt @@ -204,13 +204,13 @@ fun Calendar.timeAgoString(): String { diffDays < 1 -> "Today" diffDays == 1L -> "1 day ago" - diffDays in 2..6 -> "$diffDays days ago" + diffDays in 2L..6L -> "$diffDays days ago" diffWeeks == 1L -> "1 week ago" - diffWeeks in 2..4 -> "$diffWeeks weeks ago" + diffWeeks in 2L..4L -> "$diffWeeks weeks ago" diffMonths == 1L -> "1 month ago" - diffMonths in 2..11 -> "$diffMonths months ago" + diffMonths in 2L..11L -> "$diffMonths months ago" diffYears == 1L -> "1 year ago" diffYears > 1L -> "$diffYears years ago" diff --git a/app/src/main/res/drawable/ic_bag.png b/app/src/main/res/drawable/ic_bag.png deleted file mode 100644 index 41b1efe9c23f3c93f1ef8a6a07c9a6c3d9de8921..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2564 zcmV+f3j6hmP)?KiCDJ+(qC=NHH8 z2?W++D_pcz{P+%R-Fy!$xj_ibo1p*GJm|af2=bw{%5t#L?RL*VhLAB5kbrt5pmkr( zBXcf~g$|$Vo&1A4*FaZt|d5{4Zt^|4z zlVEE3!yEsCd}}b=-v8-3_}!bE;PaupksCMHSY7BNR1gtJP|n2dZ0qpwa0eI+!sOXUp|2BXcO_s@@@ z^UEGV0*-gOJ4etv|MBbxHjEO>xAWAK^H720nHU@cgJG-^7&}JMpa|uy!iV;MzO4{L zA#ehyZAo5w$A_elf6(Gk-2_-HvcyBHfiR4O@^aVf_Vr6*rN-6?=Pwj;6@2`0PpjI- za{^gz?C8voPy^)@moG=-q7yb^s*pS!O;uwZhN zS!nUni__>Rmqg4mUk8I=2niGtNd>MrDj;W=C^g1Z`Z)Ou3Y{uQ4ctkFLZ%^V(47RH z7%^7AFHTNCDj;W==-l33Bu8QIhsat6k%^AdYLpsuC*eV21c_9jI>SWBQcO)mm{tu0 zBh{cQ3CUDIf5}v!f|C?S4HPnqFzYlz4Z4ybfFP|#1u7D$VQ=*`R|Dt_ea(ULf+!VG zL`YQyDmYmT+%&z`qH@##$jwP^7?O%M1~r#m*gbR0S#3(?RM+0_Oj_qJ04ofCUmv z1{d351)wh}Rc=;}hWZdL)m>dpHmX;N-1>inYg)jFD8X@r@+#$`CWR;D}02 zX9?8R)qxWU(Bkx|_u$#>{)o1rFD>A3IH0P!3RVSl45!|`0*!~3nU5q7%`YHDjD z4j&vCgs!gchVJg}hE&N&>Jz9!Y!b}>aF&)x&CPE^^NHpd0tN>M`L{#78_|C4tF_>C zI$^aaD=XtAc~X@YWPV<+H|BV{zoWlC0_VjH-*FV!EU$1nX_l$z`^|@ z6NHv>$(1m-h)V(ykZZ9t(db9ZEgIwFLL_`b;RLa8AR})Dp(dcVIlC(ud1@(p?h|)Cg|~cFhbel_K}^ntR5m_YH}L7KXSL=b4G;G2oiWj$Zi+w>JLJypa&}| zT#9HTC6FygWBc92h<^6!K#d`L2zHTllw1MAe=bz}q~6>lsowbcTHb zsenQnv7QRZA^zpcC7!}k4M7Wv;d8V91&_xQa~_QznNOlWd$eic15(2D?Dn~sz9>zf zo&Fo&pA4i34E21w_=zpxctQkR8x0p{!b+eC(C(^ANC&il+IVOz?!I{PT@4N$oQO%w z2{cvz!kheDBB0QUCN(w6e?L|FB$-7$ZgL{H64VJ$s?P9XM4@%rl%C$nSN*M(QYF14 z5+XsCp_IINJZJW80jVL|9Euieb912JQ7Z%mMWWvie1amT>I)ubSk`_oyuI-U^t_5l z+nfY61*gNY6~67z<_!gp=3`BQiL@PZj51hPlHK_O0z{X;R8Z=yC#b??u>@nb;toR}O-+HbsU zlcw-z&&6JcB2Zw#iqrMf1~6xhCA>|30gFN+ZJRM^;2ttvB~T=xn?11qApEv*G$MS_ z%U3V%se|2ByCL#noT!}Pm~{y)`Z4L!l0d$SX0fh{O8EH!AC&xPxrBG(y$xW_{R!Yq z7gB-N95!wWMGKxJn$2b_jcO($L+wv%c*%{H;NzjAvw*Kxrjxd(18sz$=s;>PnX*gG zYIw{9=sK_BsGUA_nwt#IHc>$&YbweT0Y{G>0V5+-&YE1fzZ5FZ6%<%ubaY%aQ<%tP zeo%`!ei_au-g@&bG@GGtJK246b6rZ)_tij6p9y3VqoZS@`Tr3fRXZn8J;0J@$ylPc z7M4f|ISl^p|P~I{UbkFHrXY$T+p0Y5%KwmRvq++1$!bR!nXLi6> zH$R7TFm*feLJ!^8zbC$7j;jNR?3G^U+Q4=EWbJ|wWQ|lMI(_C{_%5OAqs#F3{sI2p zgt^DvGf*y7zVhM4&X((6{F7H*QpLi2s7w}}*VDY=+Tiu1N?8f0gr|KOoVIVzjX~Fk zmmwWsK?ShRIHSmN>oGM0ZyIWX4xEm aedvEA2;ig&CBxnT0000 + + + + + + + + + + + + From 80817e77c44876572f8c50f59ade179f3e2b7052 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 12 Mar 2026 20:10:19 -0400 Subject: [PATCH 08/11] Rebased and Addressed comments --- app/build.gradle | 8 +-- .../data/repositories/UserInfoRepository.kt | 52 +++++--------- .../profile/workouts/HistorySection.kt | 44 ++++++------ .../profile/workouts/WorkoutProgressArc.kt | 72 +------------------ .../ui/screens/profile/ProfileScreen.kt | 17 ++--- .../ui/viewmodels/profile/ProfileViewModel.kt | 25 ++++--- 6 files changed, 63 insertions(+), 155 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2604ce3..fdc87cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,8 +48,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' buildConfigField("String", "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"]) buildConfigField("String", "BACKEND_URL", secretsProperties['PROD_ENDPOINT']) - buildConfigField("boolean", "ONBOARDING_FLAG", "false") - buildConfigField("boolean", "CHECK_IN_FLAG", "false") + buildConfigField("boolean", "ONBOARDING_FLAG", "true") + buildConfigField("boolean", "CHECK_IN_FLAG", "true") } debug { buildConfigField("String", "BACKEND_URL", secretsProperties['DEV_ENDPOINT']) @@ -58,8 +58,8 @@ android { "GOOGLE_AUTH_CLIENT_ID", secretsProperties["GOOGLE_AUTH_CLIENT_ID"] ) signingConfig signingConfigs.debug - buildConfigField("boolean", "ONBOARDING_FLAG", "false") - buildConfigField("boolean", "CHECK_IN_FLAG", "false") + buildConfigField("boolean", "ONBOARDING_FLAG", "true") + buildConfigField("boolean", "CHECK_IN_FLAG", "true") } } compileOptions { diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index 345feb4..a02838c 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -64,7 +64,14 @@ class UserInfoRepository @Inject constructor( else { Log.d("UserInfoRepository", "Skipping goal upload") } - storeUserFields(id, name, netId, email, skip, goal) + storeUserFields( + id = userFields.id, + username = userFields.name, + netId = userFields.netId, + email = userFields.email ?: email, + skip = skip, + goal = goal + ) Log.d("UserInfoRepositoryImpl", "User created successfully") return true } catch (e: Exception) { @@ -111,11 +118,14 @@ class UserInfoRepository @Inject constructor( return try { val user = getUserByNetId(netId) ?: return false - storeId(user.id) - storeNetId(user.netId) - storeUsername(user.name) - storeEmail(user.email) - storeSkip(false) + storeUserFields( + id = user.id, + username = user.name, + netId = user.netId, + email = user.email, + skip = false, + goal = user.workoutGoal ?: 0 + ) Log.d("UserInfoRepositoryImpl", "Synced existing user to DataStore: ${user.id}") true @@ -172,36 +182,6 @@ class UserInfoRepository @Inject constructor( firebaseAuth.signOut() } - private suspend fun storeId(id: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.ID] = id - } - } - - private suspend fun storeUsername(username: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.USERNAME] = username - } - } - - private suspend fun storeNetId(netId: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.NETID] = netId - } - } - - private suspend fun storeEmail(email: String) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.EMAIL] = email - } - } - - private suspend fun storeGoal(goal: Int) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.GOAL] = goal - } - } - suspend fun storeSkip(skip: Boolean) { dataStore.edit { preferences -> preferences[PreferencesKeys.SKIP] = skip diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt index 63b2505..03d4552 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -35,7 +36,8 @@ data class HistoryItem( val gymName: String, val time: String, val date: String, - val timestamp: Long + val timestamp: Long, + val ago: String ) @Composable @@ -51,20 +53,14 @@ fun HistorySection( SectionTitleText("My Workout History", onClick) Spacer(modifier = Modifier.height(12.dp)) if (historyItems.isNotEmpty()) { - Column( + HistoryList( + historyItems = historyItems, modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - ) { - HistoryList(historyItems) - } + ) } else { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - EmptyHistorySection() - } + EmptyHistorySection() } } @@ -72,8 +68,11 @@ fun HistorySection( } @Composable -private fun HistoryList(historyItems: List) { - Column { +private fun HistoryList( + historyItems: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { historyItems.take(5).forEachIndexed { index, historyItem -> HistoryItemRow(historyItem = historyItem) if (index != historyItems.size - 1) { @@ -90,10 +89,7 @@ private fun HistoryItemRow( val gymName = historyItem.gymName val time = historyItem.time val date = historyItem.date - val calendar = Calendar.getInstance().apply { - timeInMillis = historyItem.timestamp - } - val ago = calendar.timeAgoString() + val ago = historyItem.ago Row( modifier = Modifier @@ -138,8 +134,8 @@ private fun EmptyHistorySection(){ painter = painterResource(id = R.drawable.ic_dufflebag), contentDescription = null, modifier = Modifier - .width(64.99967.dp) - .height(50.8181.dp) + .width(65.dp) + .height(51.dp) ) Spacer(modifier = Modifier.height(12.dp)) @@ -165,11 +161,11 @@ private fun EmptyHistorySection(){ private fun HistorySectionPreview() { val now = System.currentTimeMillis() val historyItems = listOf( - HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now - (1 * 24 * 60 * 60 * 1000) ), - HistoryItem("Noyes", "1:00 PM", "March 29, 2024", now - (3 * 24 * 60 * 60 * 1000)), - HistoryItem("Teagle Up", "2:00 PM", "March 29, 2024", now - (7 * 24 * 60 * 60 * 1000)), - HistoryItem("Teagle Down", "12:00 PM", "March 29, 2024", now - (15 * 24 * 60 * 60 * 1000)), - HistoryItem("Helen Newman", "10:00 AM", "March 29, 2024", now), + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now - (1 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Noyes", "1:00 PM", "March 29, 2024", now - (3 * 24 * 60 * 60 * 1000), "2 days ago"), + HistoryItem("Teagle Up", "2:00 PM", "March 29, 2024", now - (7 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Teagle Down", "12:00 PM", "March 29, 2024", now - (15 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Helen Newman", "10:00 AM", "March 29, 2024", now, "Today"), ) Column( modifier = Modifier diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt index a870c28..3f2c701 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -53,7 +52,7 @@ fun WorkoutProgressArc( workoutGoal: Int, ) { val isZero = workoutsCompleted <= 0 || workoutGoal <= 0 - val isComplete = workoutGoal > 0 && workoutsCompleted >= workoutGoal + val isComplete = workoutGoal in 1..workoutsCompleted // Calculate progress percentage val progress = when { @@ -187,75 +186,6 @@ private fun ProgressArc( } } -private fun DrawScope.drawProgressArc( - workoutsCompleted: Int, - workoutGoal: Int, - gradientBrush: Brush, - startAngle: Float, - progressAngle: Float, - topLeft: Offset, - arcSize: Size, - strokeWidth: Float -) { - if (workoutsCompleted == workoutGoal) { - drawArc( - brush = gradientBrush, - startAngle = startAngle, - sweepAngle = progressAngle, - useCenter = false, - topLeft = topLeft, - size = arcSize, - style = Stroke(width = strokeWidth, cap = StrokeCap.Round) - ) - } else { - drawArc( - color = PRIMARY_YELLOW, - startAngle = startAngle, - sweepAngle = progressAngle, - useCenter = false, - topLeft = topLeft, - size = arcSize, - style = Stroke(width = strokeWidth, cap = StrokeCap.Round) - ) - } -} - - -private fun DrawScope.drawArcSliderOuterCircle( - workoutsCompleted: Int, - workoutGoal: Int, - gradientBrush: Brush, - dotRadius: Float, - x: Float, - y: Float -) { - when (workoutsCompleted) { - workoutGoal -> { - drawCircle( - brush = gradientBrush, - radius = dotRadius, - center = Offset(x, y) - ) - } - - 0 -> { - drawCircle( - color = GRAY03, - radius = dotRadius, - center = Offset(x, y) - ) - } - - else -> { - drawCircle( - color = PRIMARY_YELLOW, - radius = dotRadius, - center = Offset(x, y) - ) - } - } -} - @Composable private fun WorkoutFractionTextSection(workoutsCompleted: Int, workoutGoal: Int, isComplete: Boolean) { Column( diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index a3b0918..c6a10e1 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -176,20 +176,13 @@ private fun ProfileScreenContentPreview() { uiState = ProfileUiState( name = "Melissa Velasquez", netId = "mv477", - totalGymDays = 42, - activeStreak = 5, + totalGymDays = 0, + activeStreak = 0, workoutGoal = 4, - historyItems = listOf( - HistoryItem( - gymName = "Teagle", - time = "10:00 AM", - date = "March 29, 2024", - timestamp = 0L - ) - ), + historyItems = emptyList(), daysOfMonth = listOf(10, 11, 12, 13, 14, 15, 16), - completedDays = listOf(true, false, true, false, true, false, false), - workoutsCompleted = 3 + completedDays = listOf(false, false, false, false, false, false, false), + workoutsCompleted = 0 ), {}, {}, diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 9b1b3bf..6fb6d76 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -10,6 +10,7 @@ import com.cornellappdev.uplift.ui.UpliftRootRoute import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.nav.RootNavigationRepository import com.cornellappdev.uplift.ui.viewmodels.UpliftViewModel +import com.cornellappdev.uplift.util.timeAgoString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -69,15 +70,17 @@ class ProfileViewModel @Inject constructor( return@launch } val historyItems = profile.workouts.map { + val workoutInstant = Instant.ofEpochMilli(it.timestamp) + val calendar = java.util.Calendar.getInstance().apply { + timeInMillis = it.timestamp + } HistoryItem( gymName = it.gymName, - time = formatTime.format( - Instant.ofEpochMilli(it.timestamp) - ), - date = formatDate.format( - Instant.ofEpochMilli(it.timestamp) - ), - timestamp = it.timestamp + time = formatTime.format(workoutInstant), + date = formatDate.format(workoutInstant), + timestamp = it.timestamp, + dayOfWeek = formatDayOfWeek.format(workoutInstant), + ago = calendar.timeAgoString() ) } @@ -149,4 +152,10 @@ class ProfileViewModel @Inject constructor( .ofPattern("MMMM d, yyyy") .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) -} \ No newline at end of file + + private val formatDayOfWeek = DateTimeFormatter + .ofPattern("EEE") + .withLocale(Locale.US) + .withZone(ZoneId.systemDefault()) +} + From 97786c85799190e9fdc1e691742256d5fb481154 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Wed, 18 Mar 2026 11:56:10 -0400 Subject: [PATCH 09/11] Refactored to new backend schema --- app/src/main/graphql/User.graphql | 11 ++- .../data/repositories/ProfileRepository.kt | 76 +++++++------------ .../ui/viewmodels/profile/ProfileViewModel.kt | 1 - 3 files changed, 36 insertions(+), 52 deletions(-) diff --git a/app/src/main/graphql/User.graphql b/app/src/main/graphql/User.graphql index ebfccc9..ec6f3b0 100644 --- a/app/src/main/graphql/User.graphql +++ b/app/src/main/graphql/User.graphql @@ -11,6 +11,9 @@ fragment userFields on User { lastGoalChange lastStreak totalGymDays + workoutHistory { + ...workoutFields + } } fragment workoutFields on Workout { @@ -43,14 +46,14 @@ query getUserByNetId($netId: String!) { } } -mutation SetWorkoutGoals($id: Int!, $workoutGoal: Int!) { - setWorkoutGoals(userId: $id, workoutGoal: $workoutGoal) { +mutation SetWorkoutGoals($userId: Int!, $workoutGoal: Int!) { + setWorkoutGoals(userId: $userId, workoutGoal: $workoutGoal) { ...userFields } } -mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $id: Int!) { - logWorkout(facilityId: $facilityId, userId: $id, workoutTime: $workoutTime) { +mutation LogWorkout($facilityId: Int!, $workoutTime: DateTime!, $userId: Int!) { + logWorkout(facilityId: $facilityId, userId: $userId, workoutTime: $workoutTime) { ...workoutFields } } diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt index 0d15d82..52250f1 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt @@ -4,12 +4,9 @@ import android.util.Log import com.apollographql.apollo.ApolloClient import com.cornellappdev.uplift.GetUserByNetIdQuery import com.cornellappdev.uplift.GetWeeklyWorkoutDaysQuery -import com.cornellappdev.uplift.GetWorkoutsByIdQuery import com.cornellappdev.uplift.SetWorkoutGoalsMutation import com.cornellappdev.uplift.data.models.ProfileData import com.cornellappdev.uplift.data.models.WorkoutDomain -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -39,51 +36,36 @@ class ProfileRepository @Inject constructor( val userId = user.id.toIntOrNull() ?: throw IllegalStateException("Invalid user ID: ${user.id}") - coroutineScope { - val workoutDeferred = async { - apolloClient.query(GetWorkoutsByIdQuery(userId)).execute() - } - val weeklyDeferred = async { - apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute() - } - - val workoutResponse = workoutDeferred.await() - if (workoutResponse.hasErrors()) { - Log.e("ProfileRepo", "Workout query errors: ${workoutResponse.errors}") - throw IllegalStateException("Workout query failed") - } - - val workouts = workoutResponse.data?.getWorkoutsById?.filterNotNull().orEmpty() - - val workoutDomain = workouts.map { - WorkoutDomain( - gymName = it.workoutFields.gymName, - timestamp = Instant.parse(it.workoutFields.workoutTime.toString()) - .toEpochMilli() - ) - } - - val weeklyResponse = weeklyDeferred.await() - if (weeklyResponse.hasErrors()) { - Log.e("ProfileRepo", "Weekly query errors: ${weeklyResponse.errors}") - throw IllegalStateException("Weekly workout days query failed") - } - - val weeklyDays = weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull().orEmpty() - - ProfileData( - name = user.name, - netId = user.netId, - encodedImage = user.encodedImage, - totalGymDays = user.totalGymDays, - activeStreak = user.activeStreak, - maxStreak = user.maxStreak, - streakStart = user.streakStart?.toString(), - workoutGoal = user.workoutGoal ?: 0, - workouts = workoutDomain, - weeklyWorkoutDays = weeklyDays + val weeklyResponse = apolloClient.query(GetWeeklyWorkoutDaysQuery(userId)).execute() + + if (weeklyResponse.hasErrors()) { + throw IllegalStateException("Weekly workout days query failed: ${weeklyResponse.errors}") + } + + val workouts = user.workoutHistory?.filterNotNull().orEmpty() + + val workoutDomain = workouts.map { + WorkoutDomain( + gymName = it.workoutFields.gymName, + timestamp = Instant.parse(it.workoutFields.workoutTime.toString()) + .toEpochMilli() ) } + + val weeklyDays = weeklyResponse.data?.getWeeklyWorkoutDays?.filterNotNull().orEmpty() + + ProfileData( + name = user.name, + netId = user.netId, + encodedImage = user.encodedImage, + totalGymDays = user.totalGymDays, + activeStreak = user.activeStreak, + maxStreak = user.maxStreak, + streakStart = user.streakStart?.toString(), + workoutGoal = user.workoutGoal ?: 0, + workouts = workoutDomain, + weeklyWorkoutDays = weeklyDays + ) }.onFailure { e -> Log.e("ProfileRepo", "Failed to load profile", e) } @@ -95,7 +77,7 @@ class ProfileRepository @Inject constructor( val response = apolloClient .mutation( SetWorkoutGoalsMutation( - id = userId, + userId = userId, workoutGoal = goal ) ) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 6fb6d76..bd907d0 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -79,7 +79,6 @@ class ProfileViewModel @Inject constructor( time = formatTime.format(workoutInstant), date = formatDate.format(workoutInstant), timestamp = it.timestamp, - dayOfWeek = formatDayOfWeek.format(workoutInstant), ago = calendar.timeAgoString() ) } From b95621c382d196bc8ec85181eac5f17521565f13 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Wed, 18 Mar 2026 11:57:44 -0400 Subject: [PATCH 10/11] Added token logic for existing users --- .../data/repositories/AuthInterceptor.kt | 1 + .../data/repositories/CheckInRepository.kt | 8 ++++- .../data/repositories/UserInfoRepository.kt | 30 ++++++++++++++++++- .../ui/screens/profile/ProfileScreen.kt | 16 +++++++--- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt index cbdbdff..a0bd4ad 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/AuthInterceptor.kt @@ -11,6 +11,7 @@ class AuthInterceptor @Inject constructor( ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val token = tokenManager.getAccessToken() + android.util.Log.d("AuthInterceptor", "token present = ${token != null}") val request = chain.request().newBuilder().apply { if (token != null) { addHeader("Authorization", "Bearer $token") diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt index 52e380d..eb31236 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt @@ -162,7 +162,13 @@ class CheckInRepository @Inject constructor( return try { val response = apolloClient - .mutation(LogWorkoutMutation(facilityId = gymId, workoutTime = time, id = userId )) + .mutation( + LogWorkoutMutation( + facilityId = gymId, + workoutTime = time, + userId = userId + ) + ) .execute() val ok = response.data?.logWorkout?.workoutFields != null && !response.hasErrors() diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index a02838c..cc81153 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -87,7 +87,7 @@ class UserInfoRepository @Inject constructor( } val goalResponse = apolloClient.mutation( SetWorkoutGoalsMutation( - id = id, + userId = id, workoutGoal = goal ) ) @@ -114,8 +114,36 @@ class UserInfoRepository @Inject constructor( } } + suspend fun loginAndStoreTokens(netId: String): Boolean { + return try { + val loginResponse = apolloClient.mutation( + LoginUserMutation(netId = netId) + ).execute() + + val loginData = loginResponse.data?.loginUser + if (loginResponse.hasErrors() || + loginData?.accessToken == null || + loginData.refreshToken == null + ) { + Log.e("UserInfoRepository", "Login failed: ${loginResponse.errors}") + return false + } + + tokenManager.saveTokens( + loginData.accessToken, + loginData.refreshToken + ) + Log.d("UserInfoRepository", "Saved backend tokens successfully") + true + } catch (e: Exception) { + Log.e("UserInfoRepository", "Error logging in user", e) + false + } + } + suspend fun syncUserToDataStore(netId: String): Boolean { return try { + if (!loginAndStoreTokens(netId)) return false val user = getUserByNetId(netId) ?: return false storeUserFields( diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index c6a10e1..30fc4e0 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -172,17 +172,25 @@ private fun ProfileScreenTopBar( @Preview @Composable private fun ProfileScreenContentPreview() { + val now = System.currentTimeMillis() + val historyItems = listOf( + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now - (1 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Noyes", "1:00 PM", "March 29, 2024", now - (3 * 24 * 60 * 60 * 1000), "2 days ago"), + HistoryItem("Teagle Up", "2:00 PM", "March 29, 2024", now - (7 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Teagle Down", "12:00 PM", "March 29, 2024", now - (15 * 24 * 60 * 60 * 1000), "1 day ago"), + HistoryItem("Helen Newman", "10:00 AM", "March 29, 2024", now, "Today"), + ) ProfileScreenContent( uiState = ProfileUiState( name = "Melissa Velasquez", netId = "mv477", - totalGymDays = 0, + totalGymDays = 1, activeStreak = 0, workoutGoal = 4, - historyItems = emptyList(), + historyItems = historyItems, daysOfMonth = listOf(10, 11, 12, 13, 14, 15, 16), - completedDays = listOf(false, false, false, false, false, false, false), - workoutsCompleted = 0 + completedDays = listOf(true, false, false, false, false, false, false), + workoutsCompleted = 1 ), {}, {}, From 422ef3e6b88ba2f0b2335c8a8f3e0f07e3348216 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 19 Mar 2026 11:52:47 -0400 Subject: [PATCH 11/11] Adressed some coderabbit comments --- .../data/repositories/DatastoreRepository.kt | 2 +- .../data/repositories/UserInfoRepository.kt | 15 ++++++++------- .../uplift/ui/screens/profile/ProfileScreen.kt | 4 ---- .../com/cornellappdev/uplift/util/Functions.kt | 6 ++---- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt index 49c11e9..f9c3428 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/DatastoreRepository.kt @@ -26,7 +26,7 @@ object PreferencesKeys { val NETID = stringPreferencesKey("netId") val EMAIL = stringPreferencesKey("email") val GOAL = intPreferencesKey("workoutGoal") - val SKIP = booleanPreferencesKey("skip") + val GOAL_SETTING_SKIPPED = booleanPreferencesKey("goalSkip") val FCM_TOKEN = stringPreferencesKey("fcmToken") val DECLINED_NOTIFICATION_PERMISSION = booleanPreferencesKey("declinedNotificationPermission") diff --git a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt index cc81153..10c510a 100644 --- a/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt +++ b/app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt @@ -14,6 +14,7 @@ import com.cornellappdev.uplift.SetWorkoutGoalsMutation import kotlinx.coroutines.flow.map; import kotlinx.coroutines.flow.firstOrNull import com.cornellappdev.uplift.data.models.UserInfo +import com.google.common.collect.Iterables.skip import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider @@ -69,7 +70,7 @@ class UserInfoRepository @Inject constructor( username = userFields.name, netId = userFields.netId, email = userFields.email ?: email, - skip = skip, + goalSkip = skip, goal = goal ) Log.d("UserInfoRepositoryImpl", "User created successfully") @@ -101,14 +102,14 @@ class UserInfoRepository @Inject constructor( } - suspend fun storeUserFields(id: String, username: String, netId: String, email: String, skip: Boolean, goal: Int) { + suspend fun storeUserFields(id: String, username: String, netId: String, email: String, goalSkip: Boolean, goal: Int) { dataStore.edit { preferences -> preferences[PreferencesKeys.ID] = id preferences[PreferencesKeys.NETID] = netId preferences[PreferencesKeys.USERNAME] = username preferences[PreferencesKeys.EMAIL] = email - preferences[PreferencesKeys.SKIP] = skip - if (!skip) { + preferences[PreferencesKeys.GOAL_SETTING_SKIPPED] = goalSkip + if (!goalSkip) { preferences[PreferencesKeys.GOAL] = goal } } @@ -151,7 +152,7 @@ class UserInfoRepository @Inject constructor( username = user.name, netId = user.netId, email = user.email, - skip = false, + goalSkip = user.workoutGoal == null, goal = user.workoutGoal ?: 0 ) @@ -212,14 +213,14 @@ class UserInfoRepository @Inject constructor( suspend fun storeSkip(skip: Boolean) { dataStore.edit { preferences -> - preferences[PreferencesKeys.SKIP] = skip + preferences[PreferencesKeys.GOAL_SETTING_SKIPPED] = skip } } suspend fun getSkipFromDataStore(): Boolean { return dataStore.data.map { preferences -> - preferences[PreferencesKeys.SKIP] + preferences[PreferencesKeys.GOAL_SETTING_SKIPPED] }.firstOrNull() ?: false } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index 30fc4e0..b5b28d0 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -88,10 +88,8 @@ private fun ProfileScreenContent( workoutGoal = uiState.workoutGoal, daysOfMonth = uiState.daysOfMonth, completedDays = uiState.completedDays, - reminderItems= emptyList(), //implement historyItems = uiState.historyItems, navigateToGoalsSection = toGoals, - navigateToRemindersSection = { /* TODO: Replace {} with viewmodel nav call */ }, navigateToHistorySection = toHistory ) @@ -105,10 +103,8 @@ private fun WorkoutsSectionContent( workoutGoal: Int, daysOfMonth: List, completedDays: List, - reminderItems: List, historyItems: List, navigateToGoalsSection: () -> Unit, - navigateToRemindersSection: () -> Unit, navigateToHistorySection: () -> Unit ) { Column( diff --git a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt index 516c1da..be4ffc2 100644 --- a/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt +++ b/app/src/main/java/com/cornellappdev/uplift/util/Functions.kt @@ -210,11 +210,9 @@ fun Calendar.timeAgoString(): String { diffWeeks in 2L..4L -> "$diffWeeks weeks ago" diffMonths == 1L -> "1 month ago" - diffMonths in 2L..11L -> "$diffMonths months ago" + diffMonths in 2L..12L -> "$diffMonths months ago" diffYears == 1L -> "1 year ago" - diffYears > 1L -> "$diffYears years ago" - - else -> "Today" + else -> "$diffYears years ago" } }