From 7310469bd096fd7cc8d6fbdc7d40b772e7b51fcc Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Fri, 10 Apr 2026 15:58:31 -0400 Subject: [PATCH 1/5] Implemented UI for workout history page --- .../uplift/ui/MainNavigationWrapper.kt | 9 + .../ui/components/general/UpliftTabRow.kt | 53 ++- .../profile/workouts/HistorySection.kt | 33 +- .../screens/profile/WorkoutHistoryScreen.kt | 348 ++++++++++++++++++ .../ui/viewmodels/profile/ProfileViewModel.kt | 5 +- .../main/res/drawable/ic_advance_month.xml | 11 + app/src/main/res/drawable/ic_back_month.xml | 11 + app/src/main/res/drawable/ic_calendar_tab.xml | 11 + app/src/main/res/drawable/ic_list_tab.xml | 21 ++ 9 files changed, 471 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt create mode 100644 app/src/main/res/drawable/ic_advance_month.xml create mode 100644 app/src/main/res/drawable/ic_back_month.xml create mode 100644 app/src/main/res/drawable/ic_calendar_tab.xml create mode 100644 app/src/main/res/drawable/ic_list_tab.xml 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 e3faa751..a0d652c4 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -47,6 +47,7 @@ import com.cornellappdev.uplift.ui.screens.onboarding.ProfileCreationScreen import com.cornellappdev.uplift.ui.screens.onboarding.SignInPromptScreen import com.cornellappdev.uplift.ui.screens.profile.ProfileScreen import com.cornellappdev.uplift.ui.screens.profile.SettingsScreen +import com.cornellappdev.uplift.ui.screens.profile.WorkoutHistoryScreen import com.cornellappdev.uplift.ui.screens.reminders.CapacityReminderScreen import com.cornellappdev.uplift.ui.screens.reminders.MainReminderScreen import com.cornellappdev.uplift.ui.screens.onboarding.WorkoutReminderOnboardingScreen @@ -265,6 +266,11 @@ fun MainNavigationWrapper( composable { SettingsScreen() } + composable { + WorkoutHistoryScreen( + onBack = { navController.popBackStack() } + ) + } composable {} composable {} } @@ -363,4 +369,7 @@ sealed class UpliftRootRoute { @Serializable data object Settings : UpliftRootRoute() + + @Serializable + data object WorkoutHistory : UpliftRootRoute() } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftTabRow.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftTabRow.kt index e6dc5696..0d40ce83 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftTabRow.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftTabRow.kt @@ -1,6 +1,12 @@ package com.cornellappdev.uplift.ui.components.general +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.TabRowDefaults.Divider +import androidx.compose.material3.Icon import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults.SecondaryIndicator @@ -11,8 +17,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +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.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -24,7 +32,12 @@ import com.cornellappdev.uplift.util.PRIMARY_YELLOW import com.cornellappdev.uplift.util.montserratFamily @Composable -fun UpliftTabRow(tabIndex: Int, tabs: List, onTabChange: (Int) -> Unit = {}) { +fun UpliftTabRow( + tabIndex: Int, + tabs: List, + icons: List? = null, + onTabChange: (Int) -> Unit = {} +) { TabRow( selectedTabIndex = tabIndex, containerColor = Color.White, @@ -43,19 +56,35 @@ fun UpliftTabRow(tabIndex: Int, tabs: List, onTabChange: (Int) -> Unit = } ) { tabs.forEachIndexed { index, title -> + val isSelected = tabIndex == index + val color = if (isSelected) PRIMARY_BLACK else GRAY04 Tab( - text = { - Text( - text = title, - color = if (tabIndex == index) PRIMARY_BLACK else GRAY04, - fontFamily = montserratFamily, - fontSize = 12.sp, - fontWeight = FontWeight.Bold - ) - }, - selected = tabIndex == index, + selected = isSelected, onClick = { onTabChange(index) }, selectedContentColor = GRAY01, + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (icons != null && index < icons.size) { + Icon( + painter = painterResource(id = icons[index]), + contentDescription = null, + tint = color, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + } + Text( + text = title, + color = color, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + } + } ) } } @@ -72,4 +101,4 @@ private fun UpliftTabRowPreview() { tabIndex = it } ) -} \ 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 03d4552e..042c1ee4 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 @@ -2,7 +2,6 @@ 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 @@ -18,7 +17,6 @@ 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 @@ -28,9 +26,9 @@ 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.GRAY04 +import com.cornellappdev.uplift.util.PRIMARY_BLACK import com.cornellappdev.uplift.util.montserratFamily -import com.cornellappdev.uplift.util.timeAgoString -import java.util.Calendar data class HistoryItem( val gymName: String, @@ -68,7 +66,7 @@ fun HistorySection( } @Composable -private fun HistoryList( +fun HistoryList( historyItems: List, modifier: Modifier = Modifier ) { @@ -76,14 +74,14 @@ private fun HistoryList( historyItems.take(5).forEachIndexed { index, historyItem -> HistoryItemRow(historyItem = historyItem) if (index != historyItems.size - 1) { - HorizontalDivider(color = GRAY01) + HorizontalDivider(color = GRAY01, thickness = 1.dp) } } } } @Composable -private fun HistoryItemRow( +fun HistoryItemRow( historyItem: HistoryItem ) { val gymName = historyItem.gymName @@ -94,23 +92,28 @@ private fun HistoryItemRow( Row( modifier = Modifier .fillMaxWidth() + .height(60.dp) .padding(vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom ) { - Column(){ + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ){ Text( text = gymName, fontFamily = montserratFamily, - fontSize = 14.sp, + fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = Color.Black + color = PRIMARY_BLACK ) Text( text = "$date ยท $time", fontFamily = montserratFamily, fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = Color.Gray + fontWeight = FontWeight.Medium, + color = GRAY04 ) } Text( @@ -124,7 +127,7 @@ private fun HistoryItemRow( } @Composable -private fun EmptyHistorySection(){ +fun EmptyHistorySection(){ Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, @@ -178,4 +181,4 @@ private fun HistorySectionPreview() { ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt new file mode 100644 index 00000000..bfecfb7d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt @@ -0,0 +1,348 @@ +package com.cornellappdev.uplift.ui.screens.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.TextAlign +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 com.cornellappdev.uplift.R +import com.cornellappdev.uplift.ui.components.general.UpliftTabRow +import com.cornellappdev.uplift.ui.components.profile.workouts.EmptyHistorySection +import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem +import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItemRow +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.LIGHT_GRAY +import com.cornellappdev.uplift.util.LIGHT_YELLOW +import com.cornellappdev.uplift.util.PRIMARY_BLACK +import com.cornellappdev.uplift.util.PRIMARY_YELLOW +import com.cornellappdev.uplift.util.montserratFamily +import java.time.Instant +import java.time.LocalDate +import java.time.YearMonth +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale + +@Composable +fun WorkoutHistoryScreen( + viewModel: ProfileViewModel = hiltViewModel(), + onBack: () -> Unit +) { + val uiState by viewModel.uiStateFlow.collectAsState() + WorkoutHistoryScreenContent(uiState = uiState, onBack = onBack) +} + +@Composable +fun WorkoutHistoryScreenContent( + uiState: ProfileUiState, + onBack: () -> Unit +) { + var selectedTab by remember { mutableStateOf(0) } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + WorkoutHistoryHeader(onBack = onBack) + UpliftTabRow( + tabIndex = selectedTab, + tabs = listOf("Calendar", "List"), + icons = listOf(R.drawable.ic_calendar_tab, R.drawable.ic_list_tab), + onTabChange = { selectedTab = it } + ) + + if (uiState.historyItems.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + EmptyHistorySection() + } + } else { + when (selectedTab) { + 0 -> WorkoutHistoryCalendarView(historyItems = uiState.historyItems) + 1 -> WorkoutHistoryListView(historyItems = uiState.historyItems) + } + } + } +} + +@Composable +private fun WorkoutHistoryHeader(onBack: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(LIGHT_GRAY) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_arrow), + contentDescription = "Back", + modifier = Modifier + .size(24.dp) + .clickable { onBack() }, + tint = PRIMARY_BLACK + ) + Text( + text = "History", + fontFamily = montserratFamily, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = PRIMARY_BLACK, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.size(24.dp)) + } +} + +@Composable +private fun WorkoutHistoryListView(historyItems: List) { + val groupedItems = remember(historyItems) { + historyItems.groupBy { + val date = Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + date.format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + ) { + item { Spacer(modifier = Modifier.height(8.dp)) } + + groupedItems.forEach { (month, items) -> + item { + Text( + text = month, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.padding(top = 8.dp) + ) + } + itemsIndexed(items) { index, item -> + HistoryItemRow(historyItem = item) + if (index < items.size - 1) { + HorizontalDivider(color = GRAY01, thickness = 1.dp) + } + } + item { Spacer(modifier = Modifier.height(24.dp)) } + } + + } +} + +@Composable +private fun WorkoutHistoryCalendarView(historyItems: List) { + var currentMonth by remember { mutableStateOf(YearMonth.now()) } + val workoutDates = remember(historyItems) { + historyItems.map { + Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + }.toSet() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Month Selector + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_month), + contentDescription = "Previous Month", + modifier = Modifier + .size(16.dp) + .clickable { currentMonth = currentMonth.minusMonths(1) }, + tint = PRIMARY_BLACK + ) + Spacer(modifier = Modifier.width(24.dp)) + Text( + text = currentMonth.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.US)), + fontFamily = montserratFamily, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = PRIMARY_BLACK, + modifier = Modifier.width(100.dp), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.width(24.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_advance_month), + contentDescription = "Next Month", + modifier = Modifier + .size(16.dp) + .clickable { currentMonth = currentMonth.plusMonths(1) }, + tint = PRIMARY_BLACK + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Days of Week Header + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + val daysOfWeek = listOf("M", "T", "W", "Th", "F", "Sa", "Su") + daysOfWeek.forEach { day -> + Text( + text = day, + modifier = Modifier.width(40.dp), + fontFamily = montserratFamily, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Calendar Grid + val daysInMonth = currentMonth.lengthOfMonth() + val firstOfMonth = currentMonth.atDay(1) + val firstDayOfWeek = (firstOfMonth.dayOfWeek.value + 6) % 7 + + val totalCells = firstDayOfWeek + daysInMonth + + LazyVerticalGrid( + columns = GridCells.Fixed(7), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + items(totalCells) { index -> + val dayOfMonth = index - firstDayOfWeek + 1 + if (dayOfMonth in 1..daysInMonth) { + val date = currentMonth.atDay(dayOfMonth) + val hasWorkout = workoutDates.contains(date) + val isToday = date == LocalDate.now() + + CalendarDayCell( + day = dayOfMonth.toString(), + isToday = isToday, + hasWorkout = hasWorkout + ) + } else { + Spacer(modifier = Modifier.size(40.dp)) + } + } + } + } +} + +@Composable +private fun CalendarDayCell( + day: String, + isToday: Boolean, + hasWorkout: Boolean +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.width(40.dp) + ) { + Box( + modifier = Modifier + .size(32.dp) + .background( + color = if (isToday) LIGHT_YELLOW else Color.Transparent, + shape = CircleShape + ) + .then( + if (isToday) Modifier.border(1.dp, PRIMARY_YELLOW, CircleShape) + else Modifier + ), + contentAlignment = Alignment.Center + ) { + Text( + text = day, + fontFamily = montserratFamily, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = PRIMARY_BLACK, + textAlign = TextAlign.Center + ) + } + if (hasWorkout) { + Box( + modifier = Modifier + .size(8.dp) + .background(color = PRIMARY_YELLOW, shape = CircleShape) + ) + } else { + Spacer(modifier = Modifier.size(8.dp)) + } + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun WorkoutHistoryScreenPreview() { + val now = System.currentTimeMillis() + val historyItems = listOf( + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now, "Today"), + HistoryItem("Noyes", "1:00 PM", "March 28, 2024", now - 86400000L, "Yesterday"), + HistoryItem("Teagle Up", "2:00 PM", "February 15, 2024", now - 4000000000L, "1 month ago"), + HistoryItem("Helen Newman", "9:30 AM", "February 10, 2024", now - 4430000000L, "1 month ago"), + HistoryItem("Morrison", "6:45 PM", "February 3, 2024", now - 5030000000L, "1 month ago"), + HistoryItem("Noyes", "4:15 PM", "January 7, 2024", now - 8000000000L, "2 months ago") + ) + + WorkoutHistoryScreenContent( + uiState = ProfileUiState(historyItems = historyItems), + onBack = {} + ) +} 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 bd907d0e..307c5078 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,9 +3,7 @@ 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 @@ -137,7 +135,7 @@ class ProfileViewModel @Inject constructor( } fun toHistory() { - // replace with the actual route once history exists + rootNavigationRepository.navigate(UpliftRootRoute.WorkoutHistory) } @@ -157,4 +155,3 @@ class ProfileViewModel @Inject constructor( .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) } - diff --git a/app/src/main/res/drawable/ic_advance_month.xml b/app/src/main/res/drawable/ic_advance_month.xml new file mode 100644 index 00000000..6365ab76 --- /dev/null +++ b/app/src/main/res/drawable/ic_advance_month.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_back_month.xml b/app/src/main/res/drawable/ic_back_month.xml new file mode 100644 index 00000000..f500d590 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_month.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_calendar_tab.xml b/app/src/main/res/drawable/ic_calendar_tab.xml new file mode 100644 index 00000000..a869fec8 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_tab.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_tab.xml b/app/src/main/res/drawable/ic_list_tab.xml new file mode 100644 index 00000000..3e291c7e --- /dev/null +++ b/app/src/main/res/drawable/ic_list_tab.xml @@ -0,0 +1,21 @@ + + + + + From 3c904b1df55ac462d6eaa086031cffa44a252d06 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Mon, 20 Apr 2026 22:01:51 -0400 Subject: [PATCH 2/5] Addressed PR comments --- .../screens/profile/WorkoutHistoryScreen.kt | 135 +++++++++++++----- 1 file changed, 97 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt index bfecfb7d..db5706d3 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt @@ -1,5 +1,6 @@ package com.cornellappdev.uplift.ui.screens.profile +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -13,18 +14,22 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -63,7 +68,7 @@ fun WorkoutHistoryScreen( viewModel: ProfileViewModel = hiltViewModel(), onBack: () -> Unit ) { - val uiState by viewModel.uiStateFlow.collectAsState() + val uiState = viewModel.collectUiStateValue() WorkoutHistoryScreenContent(uiState = uiState, onBack = onBack) } @@ -72,7 +77,7 @@ fun WorkoutHistoryScreenContent( uiState: ProfileUiState, onBack: () -> Unit ) { - var selectedTab by remember { mutableStateOf(0) } + var selectedTab by remember { mutableIntStateOf(0) } Column( modifier = Modifier @@ -92,9 +97,11 @@ fun WorkoutHistoryScreenContent( EmptyHistorySection() } } else { - when (selectedTab) { - 0 -> WorkoutHistoryCalendarView(historyItems = uiState.historyItems) - 1 -> WorkoutHistoryListView(historyItems = uiState.historyItems) + AnimatedContent(targetState = selectedTab, label = "historyTabContent") { tab -> + when (tab) { + 0 -> WorkoutHistoryCalendarView(historyItems = uiState.historyItems) + 1 -> WorkoutHistoryListView(historyItems = uiState.historyItems) + } } } } @@ -106,18 +113,23 @@ private fun WorkoutHistoryHeader(onBack: () -> Unit) { modifier = Modifier .fillMaxWidth() .background(LIGHT_GRAY) + .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_arrow), - contentDescription = "Back", - modifier = Modifier - .size(24.dp) - .clickable { onBack() }, - tint = PRIMARY_BLACK - ) + IconButton( + onClick = { onBack() }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_arrow), + contentDescription = "Back", + modifier = Modifier + .size(24.dp), + tint = PRIMARY_BLACK + ) + } + Text( text = "History", fontFamily = montserratFamily, @@ -130,14 +142,42 @@ private fun WorkoutHistoryHeader(onBack: () -> Unit) { } } +private sealed class HistoryListItem { + data class Header(val month: String) : HistoryListItem() + data class Workout( + val item: HistoryItem, + val showDivider: Boolean + ) : HistoryListItem() + data class SpacerItem(val month: String) : HistoryListItem() +} + @Composable private fun WorkoutHistoryListView(historyItems: List) { val groupedItems = remember(historyItems) { - historyItems.groupBy { - val date = Instant.ofEpochMilli(it.timestamp) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - date.format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) + historyItems + .sortedByDescending { it.timestamp } + .groupBy { historyItem -> + Instant.ofEpochMilli(historyItem.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) + } + } + + val listItems = remember(groupedItems) { + buildList { + groupedItems.forEach { (month, items) -> + add(HistoryListItem.Header(month)) + items.forEachIndexed { index, item -> + add( + HistoryListItem.Workout( + item = item, + showDivider = index < items.lastIndex + ) + ) + } + add(HistoryListItem.SpacerItem(month)) + } } } @@ -146,28 +186,46 @@ private fun WorkoutHistoryListView(historyItems: List) { .fillMaxSize() .padding(horizontal = 16.dp), ) { - item { Spacer(modifier = Modifier.height(8.dp)) } + item(key = "top_spacer") { + Spacer(modifier = Modifier.height(8.dp)) + } - groupedItems.forEach { (month, items) -> - item { - Text( - text = month, - fontFamily = montserratFamily, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - modifier = Modifier.padding(top = 8.dp) - ) + items( + items = listItems, + key = { listItem -> + when (listItem) { + is HistoryListItem.Header -> "header_${listItem.month}" + is HistoryListItem.Workout -> "workout_${listItem.item.timestamp}" + is HistoryListItem.SpacerItem -> "spacer_${listItem.month}" + } } - itemsIndexed(items) { index, item -> - HistoryItemRow(historyItem = item) - if (index < items.size - 1) { - HorizontalDivider(color = GRAY01, thickness = 1.dp) + ) { listItem -> + when (listItem) { + is HistoryListItem.Header -> { + Text( + text = listItem.month, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.padding(top = 8.dp) + ) + } + + is HistoryListItem.Workout -> { + Column { + HistoryItemRow(historyItem = listItem.item) + if (listItem.showDivider) { + HorizontalDivider(color = GRAY01, thickness = 1.dp) + } + } + } + + is HistoryListItem.SpacerItem -> { + Spacer(modifier = Modifier.height(24.dp)) } } - item { Spacer(modifier = Modifier.height(24.dp)) } } - } } @@ -191,7 +249,10 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { // Month Selector Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.spacedBy( + 24.dp, + Alignment.CenterHorizontally + ), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -202,7 +263,6 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { .clickable { currentMonth = currentMonth.minusMonths(1) }, tint = PRIMARY_BLACK ) - Spacer(modifier = Modifier.width(24.dp)) Text( text = currentMonth.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.US)), fontFamily = montserratFamily, @@ -212,7 +272,6 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { modifier = Modifier.width(100.dp), textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.width(24.dp)) Icon( painter = painterResource(id = R.drawable.ic_advance_month), contentDescription = "Next Month", From b91c17aa26f86ced2b95d9bf6c2bd8f3f6461971 Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Tue, 21 Apr 2026 20:50:31 -0400 Subject: [PATCH 3/5] Implemented logged workout dropdown functionality --- .../screens/profile/WorkoutHistoryScreen.kt | 391 +++++++++++++----- .../ui/viewmodels/profile/ProfileViewModel.kt | 46 ++- 2 files changed, 336 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt index db5706d3..0e2f128e 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt @@ -1,6 +1,11 @@ package com.cornellappdev.uplift.ui.screens.profile import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -17,11 +22,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.GenericShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -35,7 +39,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -43,14 +50,17 @@ 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.compose.collectAsStateWithLifecycle import com.cornellappdev.uplift.R import com.cornellappdev.uplift.ui.components.general.UpliftTabRow import com.cornellappdev.uplift.ui.components.profile.workouts.EmptyHistorySection import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItemRow +import com.cornellappdev.uplift.ui.viewmodels.profile.HistoryListItem 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.GRAY04 import com.cornellappdev.uplift.util.LIGHT_GRAY import com.cornellappdev.uplift.util.LIGHT_YELLOW import com.cornellappdev.uplift.util.PRIMARY_BLACK @@ -99,8 +109,8 @@ fun WorkoutHistoryScreenContent( } else { AnimatedContent(targetState = selectedTab, label = "historyTabContent") { tab -> when (tab) { - 0 -> WorkoutHistoryCalendarView(historyItems = uiState.historyItems) - 1 -> WorkoutHistoryListView(historyItems = uiState.historyItems) + 0 -> WorkoutHistoryCalendarView(workoutDates = uiState.workoutDates) + 1 -> WorkoutHistoryListView(listItems = uiState.historyListItems) } } } @@ -142,45 +152,8 @@ private fun WorkoutHistoryHeader(onBack: () -> Unit) { } } -private sealed class HistoryListItem { - data class Header(val month: String) : HistoryListItem() - data class Workout( - val item: HistoryItem, - val showDivider: Boolean - ) : HistoryListItem() - data class SpacerItem(val month: String) : HistoryListItem() -} - @Composable -private fun WorkoutHistoryListView(historyItems: List) { - val groupedItems = remember(historyItems) { - historyItems - .sortedByDescending { it.timestamp } - .groupBy { historyItem -> - Instant.ofEpochMilli(historyItem.timestamp) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - .format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) - } - } - - val listItems = remember(groupedItems) { - buildList { - groupedItems.forEach { (month, items) -> - add(HistoryListItem.Header(month)) - items.forEachIndexed { index, item -> - add( - HistoryListItem.Workout( - item = item, - showDivider = index < items.lastIndex - ) - ) - } - add(HistoryListItem.SpacerItem(month)) - } - } - } - +private fun WorkoutHistoryListView(listItems: List) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -230,15 +203,9 @@ private fun WorkoutHistoryListView(historyItems: List) { } @Composable -private fun WorkoutHistoryCalendarView(historyItems: List) { +private fun WorkoutHistoryCalendarView(workoutDates: Map>) { var currentMonth by remember { mutableStateOf(YearMonth.now()) } - val workoutDates = remember(historyItems) { - historyItems.map { - Instant.ofEpochMilli(it.timestamp) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - }.toSet() - } + var selectedDate by remember { mutableStateOf(null) } Column( modifier = Modifier @@ -249,20 +216,24 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { // Month Selector Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - 24.dp, - Alignment.CenterHorizontally - ), + horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_month), - contentDescription = "Previous Month", - modifier = Modifier - .size(16.dp) - .clickable { currentMonth = currentMonth.minusMonths(1) }, - tint = PRIMARY_BLACK - ) + IconButton( + onClick = { + currentMonth = currentMonth.minusMonths(1) + selectedDate = null + } + + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_month), + contentDescription = "Previous Month", + modifier = Modifier + .size(16.dp), + tint = PRIMARY_BLACK + ) + } Text( text = currentMonth.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.US)), fontFamily = montserratFamily, @@ -272,14 +243,21 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { modifier = Modifier.width(100.dp), textAlign = TextAlign.Center ) - Icon( - painter = painterResource(id = R.drawable.ic_advance_month), - contentDescription = "Next Month", - modifier = Modifier - .size(16.dp) - .clickable { currentMonth = currentMonth.plusMonths(1) }, - tint = PRIMARY_BLACK - ) + + IconButton( + onClick = { + currentMonth = currentMonth.plusMonths(1) + selectedDate = null + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_advance_month), + contentDescription = "Next Month", + modifier = Modifier + .size(16.dp), + tint = PRIMARY_BLACK + ) + } } Spacer(modifier = Modifier.height(32.dp)) @@ -308,36 +286,218 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { Spacer(modifier = Modifier.height(24.dp)) // Calendar Grid - val daysInMonth = currentMonth.lengthOfMonth() - val firstOfMonth = currentMonth.atDay(1) - val firstDayOfWeek = (firstOfMonth.dayOfWeek.value + 6) % 7 - - val totalCells = firstDayOfWeek + daysInMonth - - LazyVerticalGrid( - columns = GridCells.Fixed(7), + AnimatedContent( + targetState = currentMonth, + label = "calendarMonthTransition" + ) { animatedMonth -> + + val daysInMonth = animatedMonth.lengthOfMonth() + val firstOfMonth = animatedMonth.atDay(1) + val firstDayOfWeek = (firstOfMonth.dayOfWeek.value + 6) % 7 + + val weeks = remember(animatedMonth) { + val list = mutableListOf>() + var currentDay = 1 + + while (currentDay <= daysInMonth) { + val week = (0..6).map { i -> + val dayIndex = list.size * 7 + i + val dayOfMonth = dayIndex - firstDayOfWeek + 1 + + if (dayOfMonth in 1..daysInMonth) { + animatedMonth.atDay(dayOfMonth) + } else null + } + + list.add(week) + currentDay += 7 - (if (list.size == 1) firstDayOfWeek else 0) + } + + list + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + weeks.forEach { week -> + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + week.forEachIndexed { index, date -> + if (date != null) { + val hasWorkout = workoutDates.containsKey(date) + val isToday = date == LocalDate.now() + val isSelected = selectedDate == date + + CalendarDayCell( + day = date.dayOfMonth.toString(), + isToday = isToday, + hasWorkout = hasWorkout, + modifier = Modifier.clickable { + selectedDate = + if (hasWorkout && selectedDate != date) date else null + } + ) + } else { + Spacer(modifier = Modifier.size(40.dp)) + } + } + } + + // Dropdown + val selectedInThisWeek = week.find { it == selectedDate } + + AnimatedVisibility( + visible = selectedInThisWeek != null, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + selectedInThisWeek?.let { selected -> + val workouts = workoutDates[selected] ?: emptyList() + val dayOfWeekIndex = week.indexOf(selected) + + Column { + Spacer(modifier = Modifier.height(12.dp)) + workouts.forEach { workout -> + WorkoutCalendarDropdown( + historyItem = workout, + dayOfWeekIndex = dayOfWeekIndex + ) + } + } + } + } + } + } + } + } + } +} + + + @Composable +private fun WorkoutCalendarDropdown( + historyItem: HistoryItem, + dayOfWeekIndex: Int +) { + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val horizontalPadding = 16.dp + val availableWidth = screenWidth - (horizontalPadding * 2) + val cellWidth = availableWidth / 7 + + // Calculate arrow offset to point to the center of the day cell + val arrowXOffset = (cellWidth * dayOfWeekIndex) + (cellWidth / 2) + val density = LocalDensity.current + + val shape = remember(arrowXOffset, density) { + GenericShape { size, _ -> + val arrowWidth = with(density) { 12.dp.toPx() } + val arrowHeight = with(density) { 8.dp.toPx() } + val cornerRadius = with(density) { 12.dp.toPx() } + val arrowX = with(density) { arrowXOffset.toPx() } + + // Top edge with arrow + moveTo(0f, arrowHeight + cornerRadius) + arcTo( + rect = Rect(0f, arrowHeight, cornerRadius * 2, arrowHeight + cornerRadius * 2), + startAngleDegrees = 180f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + lineTo(arrowX - arrowWidth / 2, arrowHeight) + lineTo(arrowX, 0f) + lineTo(arrowX + arrowWidth / 2, arrowHeight) + lineTo(size.width - cornerRadius, arrowHeight) + arcTo( + rect = Rect(size.width - cornerRadius * 2, arrowHeight, size.width, arrowHeight + cornerRadius * 2), + startAngleDegrees = 270f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + // Right edge + lineTo(size.width, size.height - cornerRadius) + arcTo( + rect = Rect(size.width - cornerRadius * 2, size.height - cornerRadius * 2, size.width, size.height), + startAngleDegrees = 0f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + // Bottom edge + lineTo(cornerRadius, size.height) + arcTo( + rect = Rect(0f, size.height - cornerRadius * 2, cornerRadius * 2, size.height), + startAngleDegrees = 90f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + close() + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = LIGHT_YELLOW, shape = shape) + .border(width = 1.dp, color = PRIMARY_YELLOW, shape = shape) + .padding(top = 8.dp) + ) { + Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalArrangement = Arrangement.SpaceBetween + .padding(horizontal = 12.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom ) { - items(totalCells) { index -> - val dayOfMonth = index - firstDayOfWeek + 1 - if (dayOfMonth in 1..daysInMonth) { - val date = currentMonth.atDay(dayOfMonth) - val hasWorkout = workoutDates.contains(date) - val isToday = date == LocalDate.now() - - CalendarDayCell( - day = dayOfMonth.toString(), - isToday = isToday, - hasWorkout = hasWorkout + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = historyItem.gymName, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = PRIMARY_BLACK + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = historyItem.time, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = GRAY04 + ) + Spacer(modifier = Modifier.width(4.dp)) + Box( + modifier = Modifier + .size(2.dp) + .background(GRAY04, CircleShape) + ) + Spacer(modifier = Modifier.width(4.dp)) + val shortDate = remember(historyItem.timestamp) { + Instant.ofEpochMilli(historyItem.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("MMM d", Locale.US)) + } + Text( + text = shortDate, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = GRAY04 ) - } else { - Spacer(modifier = Modifier.size(40.dp)) } } + Text( + text = historyItem.ago, + fontFamily = montserratFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = PRIMARY_BLACK + ) } } } @@ -346,12 +506,13 @@ private fun WorkoutHistoryCalendarView(historyItems: List) { private fun CalendarDayCell( day: String, isToday: Boolean, - hasWorkout: Boolean + hasWorkout: Boolean, + modifier: Modifier = Modifier ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.width(40.dp) + modifier = modifier.width(40.dp) ) { Box( modifier = Modifier @@ -396,12 +557,44 @@ private fun WorkoutHistoryScreenPreview() { HistoryItem("Noyes", "1:00 PM", "March 28, 2024", now - 86400000L, "Yesterday"), HistoryItem("Teagle Up", "2:00 PM", "February 15, 2024", now - 4000000000L, "1 month ago"), HistoryItem("Helen Newman", "9:30 AM", "February 10, 2024", now - 4430000000L, "1 month ago"), - HistoryItem("Morrison", "6:45 PM", "February 3, 2024", now - 5030000000L, "1 month ago"), - HistoryItem("Noyes", "4:15 PM", "January 7, 2024", now - 8000000000L, "2 months ago") + HistoryItem("Morrison", "6:45 PM", "February 3, 2024", now - 5030000000L, "1 month ago") ) + val listItems = buildList { + historyItems + .sortedByDescending { it.timestamp } + .groupBy { + Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) + } + .forEach { (month, items) -> + add(HistoryListItem.Header(month)) + items.forEachIndexed { index, item -> + add( + HistoryListItem.Workout( + item, + index < items.lastIndex + ) + ) + } + add(HistoryListItem.SpacerItem(month)) + } + } + + val workoutDates = historyItems.groupBy { + Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + } + WorkoutHistoryScreenContent( - uiState = ProfileUiState(historyItems = historyItems), + uiState = ProfileUiState( + historyItems = historyItems, + historyListItems = listItems, + workoutDates = workoutDates + ), onBack = {} ) } 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 307c5078..ed817752 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 @@ -20,6 +20,14 @@ import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject +sealed class HistoryListItem { + data class Header(val month: String) : HistoryListItem() + data class Workout( + val item: HistoryItem, + val showDivider: Boolean + ) : HistoryListItem() + data class SpacerItem(val month: String) : HistoryListItem() +} data class ProfileUiState( val loading: Boolean = false, val error: Boolean = false, @@ -34,7 +42,9 @@ data class ProfileUiState( val historyItems: List = emptyList(), val daysOfMonth: List = emptyList(), val completedDays: List = emptyList(), - val workoutsCompleted: Int = 0 + val workoutsCompleted: Int = 0, + val historyListItems: List = emptyList(), + val workoutDates: Map> = emptyMap() ) @HiltViewModel @@ -96,6 +106,36 @@ class ProfileViewModel @Inject constructor( val workoutsCompleted = profile.weeklyWorkoutDays.size + val grouped = historyItems + .sortedByDescending { it.timestamp } + .groupBy { + Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.US)) + } + + val historyListItems = buildList{ + grouped.forEach { (month, items) -> + add(HistoryListItem.Header(month)) + items.forEachIndexed { index, item -> + add( + HistoryListItem.Workout( + item = item, + showDivider = index < items.lastIndex + ) + ) + } + add(HistoryListItem.SpacerItem(month)) + } + } + + val workoutDates = historyItems.groupBy { + Instant.ofEpochMilli(it.timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + } + applyMutation { copy( loading = false, @@ -110,7 +150,9 @@ class ProfileViewModel @Inject constructor( historyItems = historyItems, daysOfMonth = daysOfMonth, completedDays = completedDays, - workoutsCompleted = workoutsCompleted + workoutsCompleted = workoutsCompleted, + historyListItems = historyListItems, + workoutDates = workoutDates ) } From c0aabc16f682f07ece079dc1cb885d99fde4530e Mon Sep 17 00:00:00 2001 From: Melissa Velasquez Date: Thu, 23 Apr 2026 17:27:20 -0400 Subject: [PATCH 4/5] Pushed back logic to VM and changed animation --- .../profile/workouts/HistorySection.kt | 13 ++-- .../ui/screens/profile/ProfileScreen.kt | 10 +-- .../screens/profile/WorkoutHistoryScreen.kt | 70 +++++++++++-------- .../ui/viewmodels/profile/ProfileViewModel.kt | 9 ++- 4 files changed, 61 insertions(+), 41 deletions(-) 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 042c1ee4..f4c97f01 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 @@ -35,7 +35,8 @@ data class HistoryItem( val time: String, val date: String, val timestamp: Long, - val ago: String + val ago: String, + val shortDate: String ) @Composable @@ -164,11 +165,11 @@ 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), "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"), + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now - (1 * 24 * 60 * 60 * 1000), "1 day ago", "Mar 28"), + HistoryItem("Noyes", "1:00 PM", "March 29, 2024", now - (3 * 24 * 60 * 60 * 1000), "2 days ago", "Mar 26"), + HistoryItem("Teagle Up", "2:00 PM", "March 29, 2024", now - (7 * 24 * 60 * 60 * 1000), "1 week ago", "Mar 22"), + HistoryItem("Teagle Down", "12:00 PM", "March 29, 2024", now - (15 * 24 * 60 * 60 * 1000), "2 weeks ago", "Mar 14"), + HistoryItem("Helen Newman", "10:00 AM", "March 29, 2024", now, "Today", "Mar 29"), ) Column( modifier = Modifier 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 b5b28d04..de192439 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 @@ -170,11 +170,11 @@ private fun ProfileScreenTopBar( 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"), + HistoryItem("Morrison", "11:00 PM", "March 29, 2024", now, "Today", "Mar 29"), + HistoryItem("Noyes", "1:00 PM", "March 28, 2024", now - 86400000L, "Yesterday", "Mar 28"), + HistoryItem("Teagle Up", "2:00 PM", "February 15, 2024", now - 4000000000L, "1 month ago", "Feb 15"), + HistoryItem("Helen Newman", "9:30 AM", "February 10, 2024", now - 4430000000L, "1 month ago", "Feb 10"), + HistoryItem("Morrison", "6:45 PM", "February 3, 2024", now - 5030000000L, "1 month ago", "Feb 3") ) ProfileScreenContent( uiState = ProfileUiState( diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt index 0e2f128e..380fdaea 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/WorkoutHistoryScreen.kt @@ -2,10 +2,14 @@ package com.cornellappdev.uplift.ui.screens.profile import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -332,7 +336,6 @@ private fun WorkoutHistoryCalendarView(workoutDates: Map - val workouts = workoutDates[selected] ?: emptyList() - val dayOfWeekIndex = week.indexOf(selected) - - Column { - Spacer(modifier = Modifier.height(12.dp)) - workouts.forEach { workout -> - WorkoutCalendarDropdown( - historyItem = workout, - dayOfWeekIndex = dayOfWeekIndex - ) + AnimatedVisibility( + visible = selectedInThisWeek != null, + enter = fadeIn(tween(300)) + expandVertically( + expandFrom = Alignment.Top, + animationSpec = tween(350) + ), + exit = fadeOut(tween(800)) + shrinkVertically( + shrinkTowards = Alignment.Top, + animationSpec = tween(700) + ) + ) { + selectedInThisWeek?.let { selected -> + val workouts = workoutDates[selected] ?: emptyList() + val dayOfWeekIndex = week.indexOf(selected) + + Column { + Spacer(modifier = Modifier.height(12.dp)) + + workouts.forEach { workout -> + WorkoutCalendarDropdown( + historyItem = workout, + dayOfWeekIndex = dayOfWeekIndex + ) + } } } } } + } } } @@ -380,7 +397,7 @@ private fun WorkoutHistoryCalendarView(workoutDates: Map Date: Thu, 23 Apr 2026 17:37:31 -0400 Subject: [PATCH 5/5] formatted date wong --- .../uplift/ui/viewmodels/profile/ProfileViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e56ff7a..76b15adb 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 @@ -200,7 +200,7 @@ class ProfileViewModel @Inject constructor( .withZone(ZoneId.systemDefault()) private val shortDateFormatter = DateTimeFormatter - .ofPattern("MMM, d") + .ofPattern("MMM d") .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) }