Skip to content

Profile Page Implementation#105

Open
melissavelasquezz wants to merge 8 commits intomainfrom
melissa/workout-history-ui
Open

Profile Page Implementation#105
melissavelasquezz wants to merge 8 commits intomainfrom
melissa/workout-history-ui

Conversation

@melissavelasquezz
Copy link
Contributor

Overview

Profile Page implementation that aligns with the latest backend schema and new designs.

Changes Made

Updated the schema and user GraphQL queries/models to match the latest backend changes

Updated the User data model/class accordingly

Reworked the profile page UI and components to match the latest designs

Implemented the Profile ViewModel and repository logic to load profile data from backend

Added additional testing/debugging work around workout logging through the Check-In ViewModel and repository

Test Coverage

Manually tested

Next Steps (delete if not applicable)

Further test profile page and workout history once logWorkout is fully authenticated and working

Add networking and logic for settings and full workout history

Add loading page

Screenshots

Screen Shot\
profilepagess

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the new Profile page end-to-end (UI + ViewModel + repository) and updates GraphQL schema/queries/models to match the latest backend changes.

Changes:

  • Added profile data loading via ProfileRepository + ProfileViewModel and wired it into the Profile screen UI.
  • Updated GraphQL schema and User/Workout fragments + mutations to align with backend changes (e.g., workoutGoal as Int).
  • Added/updated supporting UI components (history empty state, weekly progress, progress arc) and small logging/debug improvements in Check-In and login flows.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
app/src/main/res/drawable/ic_bag.png Adds an empty-state icon for workout history.
app/src/main/java/com/cornellappdev/uplift/util/Functions.kt Adds Calendar.timeAgoString() helper for history “time ago” text.
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt New Profile VM: loads profile + formats workout history + computes weekly progress state.
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/CheckInViewModel.kt Logs success/failure of backend workout logging.
app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/onboarding/LoginViewModel.kt Syncs existing user data into DataStore before navigating to Home.
app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt Reworks Profile screen to use ProfileViewModel state and new navigation callbacks.
app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutSection.kt Adds a workouts section composable (currently duplicated).
app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WorkoutProgressArc.kt Improves arc rendering for zero/complete goal cases; tweaks labels.
app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/WeeklyProgressTracker.kt Adds guard for insufficient day data.
app/src/main/java/com/cornellappdev/uplift/ui/components/profile/workouts/HistorySection.kt Adds “time ago” display and empty state UI for history.
app/src/main/java/com/cornellappdev/uplift/ui/components/profile/ProfileHeaderSection.kt Updates header to display netId and handle long names.
app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt Wires ProfileScreen into navigation and introduces ProfileViewModel creation in wrapper.
app/src/main/java/com/cornellappdev/uplift/data/repositories/UserInfoRepository.kt Fixes create-user DataStore writes; adds syncUserToDataStore; expands user model mapping.
app/src/main/java/com/cornellappdev/uplift/data/repositories/ProfileRepository.kt New repository fetching user/workouts/weekly days and setting workout goal.
app/src/main/java/com/cornellappdev/uplift/data/repositories/CheckInRepository.kt Improves userId parsing/logging and adds extra error logging for logWorkout.
app/src/main/java/com/cornellappdev/uplift/data/models/UserInfo.kt Expands UserInfo model to include profile-related fields.
app/src/main/graphql/schema.graphqls Updates schema for user streak/goal fields, workout fields, and mutations.
app/src/main/graphql/User.graphql Updates fragments/queries/mutations (e.g., workoutGoal: Int, adds workout gymName).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -1,10 +1,18 @@
package com.cornellappdev.uplift.data.models
import com.cornellappdev.uplift.type.DateTime
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

com.cornellappdev.uplift.type.DateTime is imported but not used in this data class. Remove the unused import to avoid lint warnings and keep the model minimal.

Suggested change
import com.cornellappdev.uplift.type.DateTime

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Comment on lines +62 to +70
val historyItems = profile.workouts.map {
HistoryItem(
gymName = it.gymName,
time = formatTime.format(
Instant.ofEpochMilli(it.timestamp)
),
date = formatDate.format(
Instant.ofEpochMilli(it.timestamp)
),
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HistoryItem is built with both time (formatted as h:mm a) and date formatted as MMMM d, yyyy • h:mm a. In HistoryItemRow the UI renders "$date · $time", which will duplicate the time portion (e.g., "March 29, 2024 • 1:00 PM · 1:00 PM"). Either remove the time from formatDate or stop appending time in the row.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +91
val checkInUiState = checkInViewModel.collectUiStateValue()
val confettiUiState = confettiViewModel.collectUiStateValue()

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkInViewModel and confettiViewModel are referenced here but are not declared in MainNavigationWrapper (they’re only created later inside the Box when CHECK_IN_FLAG is true). This will not compile; remove these lines or instantiate the view models before collecting their UI state (and avoid duplicating the later declarations).

Suggested change
val checkInUiState = checkInViewModel.collectUiStateValue()
val confettiUiState = confettiViewModel.collectUiStateValue()

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +24
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
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import androidx.compose.ui.focus.focusModifier (and some of the other new imports around it) is unused here, and focusModifier is not part of the public Compose API in recent versions—this may fail compilation. Please remove the unused/invalid imports to avoid build errors.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +95
activeStreak = user.activeStreak ?: 0,
maxStreak = user.maxStreak ?: 0,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the updated GraphQL schema, activeStreak/maxStreak are now non-null (Int!). After regeneration, these fields will be non-null Kotlin Ints, so using the Elvis operator (user.activeStreak ?: 0) will not compile. Update this mapping to treat them as non-null (and only use null-coalescing for fields that remain nullable, e.g. workoutGoal).

Suggested change
activeStreak = user.activeStreak ?: 0,
maxStreak = user.maxStreak ?: 0,
activeStreak = user.activeStreak,
maxStreak = user.maxStreak,

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +93
val profileViewModel: ProfileViewModel = hiltViewModel()

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProfileViewModel is being created in MainNavigationWrapper via hiltViewModel(), even though this file’s own comment says future view models should be added in the screen they’re used in. Consider removing this and letting ProfileScreen create its own hiltViewModel() (or instantiate it inside the composable<UpliftRootRoute.Profile> block) so the ViewModel is properly scoped to the Profile destination.

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 14
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
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several unused imports here (e.g. android.R.attr.name, windowInsetsEndWidth, rememberScrollState, verticalScroll, viewModel, ProfileRepository, UserInfoRepository, etc.). Please remove unused imports to keep the file clean and avoid lint/CI failures if unused-imports are enforced.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +41
import androidx.compose.ui.unit.dp

@Composable
private fun WorkoutsSectionContent(
workoutsCompleted: Int,
workoutGoal: Int,
daysOfMonth: List<Int>,
completedDays: List<Boolean>,
reminderItems: List<ReminderItem>,
historyItems: List<HistoryItem>,
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
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new file duplicates the WorkoutsSectionContent composable that is already defined/used in ProfileScreen.kt, but this version is private and is not referenced anywhere. Consider deleting this file or making it the single shared implementation to avoid dead code and future drift between two copies.

Suggested change
import androidx.compose.ui.unit.dp
@Composable
private fun WorkoutsSectionContent(
workoutsCompleted: Int,
workoutGoal: Int,
daysOfMonth: List<Int>,
completedDays: List<Boolean>,
reminderItems: List<ReminderItem>,
historyItems: List<HistoryItem>,
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)
)
}
}
import androidx.compose.ui.unit.dp

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
import android.R.attr.fontFamily
import android.R.attr.fontWeight
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android.R.attr.fontFamily and android.R.attr.fontWeight are imported but not used. Please remove these unused imports (and prefer Compose’s FontFamily/FontWeight types when needed) to keep the file clean.

Suggested change
import android.R.attr.fontFamily
import android.R.attr.fontWeight

Copilot uses AI. Check for mistakes.
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}")
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the full GraphQL response.data on failures can leak user/workout data into logs (which may be captured on devices or in crash reports). Prefer logging only the error messages/codes, or gate verbose response logging behind a debug-only flag.

Suggested change
Log.e("CheckInRepository", "LogWorkout response data=${response.data}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +207 to +213
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"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeAgoString() uses diffDays in 2..6, diffWeeks in 2..4, and diffMonths in 2..11 but diffDays/diffWeeks/diffMonths are Long. This won’t compile because those ranges are IntRange. Use 2L..6L / 2L..4L / 2L..11L (or convert the diffs to Int safely) so the when branches type-check.

Suggested change
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"
diffDays in 2L..6L -> "$diffDays days ago"
diffWeeks == 1L -> "1 week ago"
diffWeeks in 2L..4L -> "$diffWeeks weeks ago"
diffMonths == 1L -> "1 month ago"
diffMonths in 2L..11L -> "$diffMonths months ago"

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Comment on lines +80 to +81
val confettiViewModel: ConfettiViewModel = hiltViewModel()
val checkInViewModel: CheckInViewModel = hiltViewModel()
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at the top of MainNavigationWrapper asks that new view models be created in the screens where they’re used, but this adds ConfettiViewModel/CheckInViewModel here. These two instances are also unused because new instances are created again inside the CHECK_IN_FLAG block, which will trigger unused-variable lint warnings. Remove the top-level hiltViewModel() calls and keep the scoped ones (or pass the single instances down so you don’t create duplicates).

Suggested change
val confettiViewModel: ConfettiViewModel = hiltViewModel()
val checkInViewModel: CheckInViewModel = hiltViewModel()

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might change after check in flag is removed

Comment on lines +76 to +80
gymDays = uiState.totalGymDays,
streaks = uiState.activeStreak,
profilePictureUri = uiState.profileImage?.let { Uri.parse(it) },
onPhotoSelected = {},
netId = uiState.netId
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

profileImage is coming from encodedImage in the backend model; parsing it as a Uri and feeding it into PhotoPicker/Coil likely won’t render unless the backend value is a real URI (e.g., content://, file://, or https://). If this is base64/encoded content (as the name suggests), decode it to an ImageBitmap/ByteArray for display or change PhotoPicker to accept a raw model (e.g., String/data URI) rather than forcing a Uri.parse(...).

Copilot uses AI. Check for mistakes.
Comment on lines +52 to 54
if (daysOfMonth.size < daysOfWeek.size) {
return
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WeeklyProgressTracker returns early when daysOfMonth.size < daysOfWeek.size, which means the entire tracker silently disappears instead of rendering partial/padded data. Consider padding daysOfMonth the same way completedDays is padded (or showing a fallback UI) so the component always renders predictably.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ Maybe ask design for how we should handle this edge case?

Comment on lines +122 to +144
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)
)
}
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After switching ProgressArc to draw the arc/circle directly, the private helpers drawProgressArc(...) and drawArcSliderOuterCircle(...) appear to no longer be used anywhere in this file. Removing them (or reusing them from the new implementation) will avoid dead code and potential lint warnings.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to +53
name: String,
gymDays: Int,
streaks: Int,
badges: Int
netID: String,
modifier: Modifier = Modifier
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProfileHeaderInfoDisplay uses the parameter name netID, while the rest of the codebase (and the public ProfileHeaderSection API) uses netId. Renaming this parameter to netId would keep casing consistent and avoid confusion when wiring props through.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +112
Column(
modifier = Modifier.fillMaxSize()
) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkoutsSectionContent uses Column(modifier = Modifier.fillMaxSize()) but it’s placed inside the main ProfileScreen Column without a weight. With the outer screen no longer scrollable, this can cause the workouts section to measure at full screen height in addition to the header, leading to clipped/overflowing content. Consider removing fillMaxSize() here and/or giving WorkoutsSectionContent a Modifier.weight(1f) from the parent so it only consumes the remaining space.

Suggested change
Column(
modifier = Modifier.fillMaxSize()
) {
Column {

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to export as svg instead?

}
composable<UpliftRootRoute.Profile> {
ProfileScreen()
ProfileScreen(toSettings = {}, toGoals = {}, toHistory = {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to make use of the root navigation repository for navigation in the vm instead of passing from here

suspend fun logWorkoutFromCheckIn(gymId: Int): Boolean {
val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return false
val userIdString = userInfoRepository.getUserIdFromDataStore()
val userId = userIdString?.toIntOrNull()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Not sure if you need to separate into two variables if only used once

import javax.inject.Inject
import javax.inject.Singleton

data class ProfileData(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the data classes, it might be good to separate out into the models folder

private val apolloClient: ApolloClient
) {
suspend fun getProfile(): Result<ProfileData> {
return try{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend runCatching over try catch

}

suspend fun setWorkoutGoal(userId: Int, goal: Int): Result<Unit> {
return try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recommend runCatching here

) {
suspend fun getProfile(): Result<ProfileData> {
return try{
val netId = userInfoRepository.getNetIdFromDataStore()
Copy link
Member

@AndrewCheung360 AndrewCheung360 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: For several of the function calls or apollo queries, it might be good to separate it out from this function for better separation and clarity

Log.e("ProfileRepo", "Workout query errors: ${workoutResponse.errors}")
}

val workouts = if (workoutResponse.hasErrors()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a couple of the hasErrors in this function only log or just set to empty list, but we should probably be throwing exceptions and handling them in the vm

val workoutDomain = workouts.map {
WorkoutDomain(
gymName = it.workoutFields.gymName,
timestamp = Instant.parse(it.workoutFields.workoutTime.toString()).toEpochMilli()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need. to convert workout time to string before the conversion or can we do this directly?

).execute()

if (userResponse.hasErrors()) {
Log.e("ProfileRepo", "User query errors: ${userResponse.errors}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: for "ProfileRepo" you can create a private const val TAG

.execute()

if (response.hasErrors()) {
Result.failure(Exception("Goal update failed"))
Copy link
Member

@AndrewCheung360 AndrewCheung360 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: for the exceptions like this one and the other ones in the file, it's fine how you have it, but if it is a specific one, you can probably create exception classes to better handle them specifically instead of catching general exceptions and relying on the message

val userId = user.id.toIntOrNull()
?: return Result.failure(IllegalStateException("Invalid user ID: ${user.id}"))

val workoutResponse = apolloClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these queries can be run in parallel to save time eg. Workouts and Weekly

return try {
val user = getUserByNetId(netId) ?: return false

storeId(user.id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can combine into a singular disk write instead of multiple disk writes (eg. multiple datastore.edit calls instead of one)

storeUsername(name)
storeEmail(email)

val createdUser = response.data?.createUser?.userFields ?: return false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this file in create user, you might want to work w/ preston or check his PR branch out just to ensure there is no merge conflicts or overlap here

painter = painterResource(id = R.drawable.ic_bag),
contentDescription = null,
modifier = Modifier
.width(64.99967.dp)
Copy link
Member

@AndrewCheung360 AndrewCheung360 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very specific values lol, can we round to the nearest int or an even num?

HistoryList(historyItems)
}
} else {
Box(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this box needed?

SectionTitleText("My Workout History", onClick)
Spacer(modifier = Modifier.height(12.dp))
if (historyItems.isNotEmpty()) {
Column(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Column needed or could you just apply the styling to the history list composable itself?

val time = historyItem.time
val dayOfWeek = historyItem.dayOfWeek
val date = historyItem.date
val calendar = Calendar.getInstance().apply {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too ideal to recompute every time it recomposes, so could use remember or format in the VM


Box(modifier = Modifier.fillMaxWidth()) {
ConnectingLines(daysOfWeek, lastCompletedIndex)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we could remove space

.coerceIn(0f, 1f)
}
// Setup animation
val animatedProgress = remember { Animatable(0f) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to add progress to remember key param to animate from previous progress

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if drawProgressArc or drawArcSliderOuterCircle will not be used in the future, we can get rid of them

// Calculate progress percentage
val progress = (workoutsCompleted.toFloat() / workoutGoal.toFloat()).coerceIn(0f, 1f)
val isZero = workoutsCompleted <= 0 || workoutGoal <= 0
val isComplete = workoutGoal > 0 && workoutsCompleted >= workoutGoal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can simplify to workoutGoal in 1..workoutsCompleted

HistoryItem("Teagle Down", "12:00 PM", "Fri", "March 29, 2024"),
HistoryItem("Helen Newman", "10:00 AM", "Fri", "March 29, 2024"),
)
fun ProfileScreen(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could still have a preview if we extract the UI screen content w/o the vm into another composable and called it in profile screen while passing in whatever states or functions it needs from the vm

)
fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel(),
toSettings:() -> Unit,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not need these nav functions here if we use rootNavigationRepository in the vm

val uiState by viewModel.uiStateFlow.collectAsState()

val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should trigger the reload here. Maybe we can put it in the init of the vm instead

val result = profileRepository.getProfile()

if (result.isSuccess) {
val profile = result.getOrNull()!!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably better to not use the !!: val profile = result.getOrNull() ?: return@launch

HistoryItem(
gymName = it.gymName,
time = formatTime.format(
Instant.ofEpochMilli(it.timestamp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this instant is reused a couple of times, so we can make a variable to share



fun updateWorkoutGoal(goal: Int) = viewModelScope.launch {
val userId = userInfoRepository.getUserIdFromDataStore()?.toIntOrNull() ?: return@launch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we only need userId from userInfoRepository, it might be better to just change profile repository setgoal function to just take a goal and get the id in the data layer to avoid injecting another repo to this vm

name = uiState.name,
gymDays = uiState.totalGymDays,
streaks = uiState.activeStreak,
profilePictureUri = uiState.profileImage?.let { Uri.parse(it) },
Copy link
Member

@AndrewCheung360 AndrewCheung360 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably shouldn't do parsing in the UI and let vm or data layer handle the conversion and just take in the Uri

@melissavelasquezz melissavelasquezz force-pushed the melissa/workout-history-ui branch from 361c307 to 80817e7 Compare March 13, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants