From 3c52ba286343ad937beb830e7ab97caac6a92907 Mon Sep 17 00:00:00 2001 From: amjiao Date: Tue, 20 Jan 2026 13:12:36 -0500 Subject: [PATCH 01/10] small nav fix --- .../java/com/cornellappdev/score/nav/root/RootNavigation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt index b74f870..a837957 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt @@ -124,7 +124,7 @@ fun NavBackStackEntry.toScreen(): ScoreScreens? = "ScoresScreen" -> toRoute() "GameScoreSummaryPage" -> toRoute() "HighlightsScreen" -> toRoute() - "HighlightsSearchScreen" -> toRoute() + "HighlightsSearchScreen" -> toRoute() else -> throw IllegalArgumentException("Invalid screen") } From e02cc5df5441b759ab941ef7172bc4b6445fdd23 Mon Sep 17 00:00:00 2001 From: amjiao Date: Fri, 30 Jan 2026 14:43:21 -0500 Subject: [PATCH 02/10] UI Fixes --- .../components/highlights/HighlightsFilter.kt | 29 ++++++++++++------- .../HighlightsScreenSearchFilterBar.kt | 6 ++-- .../highlights/HighlightsSearchBar.kt | 9 +++--- .../highlights/VideoHighlightsCard.kt | 26 ++++++++++------- .../score/screen/HighlightsSearchScreen.kt | 12 ++++---- .../score/screen/HighlightsSubScreen.kt | 6 ++-- .../cornellappdev/score/screen/HomeScreen.kt | 2 +- 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsFilter.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsFilter.kt index ea273ff..69172aa 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsFilter.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsFilter.kt @@ -27,24 +27,26 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.GrayLight import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.Stroke import com.cornellappdev.score.theme.Style.bodyNormal import com.cornellappdev.score.theme.White import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList @Composable private fun HighlightsFilterButton( sport: Sport, - onFilterSelected: (Sport) -> Unit, + onFilterSelected: (SportSelection) -> Unit, isSelected: Boolean = false, ) { OutlinedButton( modifier = Modifier .height(32.dp), border = BorderStroke(width = 1.dp, color = Stroke), - onClick = { onFilterSelected(sport) }, + onClick = { onFilterSelected(SportSelection.SportSelect(sport)) }, shape = RoundedCornerShape(100.dp), colors = outlinedButtonColors( containerColor = if (isSelected) GrayLight else White, @@ -67,21 +69,26 @@ private fun HighlightsFilterButton( @Composable fun HighlightsFilterRow( - sportList: List, - onFilterSelected: (Sport) -> Unit, + sportList: List, + onFilterSelected: (SportSelection) -> Unit, ) { LazyRow( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(start = 24.dp, end = 24.dp), verticalAlignment = Alignment.CenterVertically ) { - item { Spacer(Modifier.width(12.dp)) } - items(sportList) { item -> - HighlightsFilterButton(item, onFilterSelected) + items( + items = sportList.filterIsInstance(), + key = { it.sport } + ) { selection -> + HighlightsFilterButton( + sport = selection.sport, + onFilterSelected = onFilterSelected + ) } - item { Spacer(Modifier.width(12.dp)) } } + } @Preview @@ -94,5 +101,5 @@ private fun HighlightsFilterButtonPreview() { @Preview @Composable private fun HighlightsFilterRowPreview() { - HighlightsFilterRow(sportList, {}) + HighlightsFilterRow(sportSelectionList, {}) } diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsScreenSearchFilterBar.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsScreenSearchFilterBar.kt index d5344b8..d97ed85 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsScreenSearchFilterBar.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsScreenSearchFilterBar.kt @@ -11,11 +11,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList @Composable fun HighlightsScreenSearchFilterBar( - sportList: List + sportList: List ) { Column(modifier = Modifier.fillMaxWidth()) { HighlightsSearchBar(modifier = Modifier.padding(horizontal = 24.dp)) @@ -28,6 +30,6 @@ fun HighlightsScreenSearchFilterBar( @Composable private fun HighlightsScreenSearchFilterBarPreview() { ScorePreview { - HighlightsScreenSearchFilterBar(sportList) + HighlightsScreenSearchFilterBar(sportSelectionList) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt index 2297e77..3e47dba 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt @@ -48,7 +48,7 @@ private fun Modifier.highlightsSearchRowModifier(): Modifier = this .background(Color.White, RoundedCornerShape(100.dp)) .border(1.dp, GrayLight, RoundedCornerShape(100.dp)) .clip(RoundedCornerShape(100.dp)) - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = 8.dp, vertical = 8.dp) @Composable fun HighlightsSearchBar( @@ -89,8 +89,7 @@ fun HighlightsSearchBar( Row( modifier = Modifier - .highlightsSearchRowModifier() - .padding(horizontal = 8.dp), + .highlightsSearchRowModifier(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -103,7 +102,7 @@ fun HighlightsSearchBar( contentDescription = "search icon", tint = Color.Unspecified ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(4.dp)) Box { innerTextField() if (searchQuery.isEmpty()) { @@ -162,7 +161,7 @@ fun HighlightsSearchEntryPointRow( .clickable { onClick() } .then(modifier), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( painter = painterResource(R.drawable.search), diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt index 44c4efb..d1bc359 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt @@ -94,17 +94,21 @@ fun VideoHighlightCardBody( Row( verticalAlignment = Alignment.CenterVertically ) { - Icon( - painter = painterResource(videoHighlight.sport.emptyIcon), - contentDescription = "Sport icon", - modifier = Modifier.size(24.dp), - tint = Color.Unspecified - ) - Icon( - painter = painterResource(if (videoHighlight.gender == GenderDivision.FEMALE) R.drawable.ic_gender_women else R.drawable.ic_gender_men), - contentDescription = "Gender icon", - tint = Color.Unspecified - ) + if (videoHighlight.sport != null) { + Icon( + painter = painterResource(videoHighlight.sport.emptyIcon), + contentDescription = "Sport icon", + modifier = Modifier.size(24.dp), + tint = Color.Unspecified + ) + } + if (videoHighlight.gender != null) { + Icon( + painter = painterResource(if (videoHighlight.gender == GenderDivision.FEMALE) R.drawable.ic_gender_women else R.drawable.ic_gender_men), + contentDescription = "Gender icon", + tint = Color.Unspecified + ) + } } } Spacer(Modifier.height(8.dp)) diff --git a/app/src/main/java/com/cornellappdev/score/screen/HighlightsSearchScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HighlightsSearchScreen.kt index 0fb0bc8..d4bec11 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HighlightsSearchScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HighlightsSearchScreen.kt @@ -24,14 +24,16 @@ import com.cornellappdev.score.components.highlights.HighlightsCardLazyColumnRes import com.cornellappdev.score.components.highlights.HighlightsScreenSearchFilterBar import com.cornellappdev.score.model.HighlightData import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.heading2 import com.cornellappdev.score.util.highlightsList import com.cornellappdev.score.util.recentSearchList import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList @Composable fun HighlightsSearchScreen( - sportList: List, + sportList: List, recentSearchList: List, highlightsList: List, query: String, @@ -61,7 +63,7 @@ fun HighlightsSearchScreen( } data class HighlightsSearchScreenPreviewData( - val sportList: List, + val sportList: List, val recentSearchList: List, val query: String ) @@ -69,9 +71,9 @@ data class HighlightsSearchScreenPreviewData( class HighlightsSearchScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequence { - yield(HighlightsSearchScreenPreviewData(sportList, recentSearchList, "")) - yield(HighlightsSearchScreenPreviewData(sportList, recentSearchList, "Sports")) - yield(HighlightsSearchScreenPreviewData(sportList, recentSearchList, "Hockey")) + yield(HighlightsSearchScreenPreviewData(sportSelectionList, recentSearchList, "")) + yield(HighlightsSearchScreenPreviewData(sportSelectionList, recentSearchList, "Sports")) + yield(HighlightsSearchScreenPreviewData(sportSelectionList, recentSearchList, "Hockey")) } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/HighlightsSubScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HighlightsSubScreen.kt index 2ec4a40..d7c66c6 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HighlightsSubScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HighlightsSubScreen.kt @@ -30,11 +30,13 @@ import com.cornellappdev.score.components.highlights.HighlightsCardLazyColumn import com.cornellappdev.score.components.highlights.HighlightsScreenSearchFilterBar import com.cornellappdev.score.model.HighlightData import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.heading2 import com.cornellappdev.score.theme.White import com.cornellappdev.score.util.highlightsList import com.cornellappdev.score.util.recentSearchList import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList @Composable private fun HighlightsSubScreenHeader( @@ -80,7 +82,7 @@ private fun HighlightsSubScreenHeaderPreview() { @Composable fun HighlightsSubScreen( - sportList: List, + sportList: List, recentSearchList: List, highlightsList: List, query: String, @@ -114,7 +116,7 @@ fun HighlightsSubScreen( @Composable private fun HighlightsSubScreenPreview() { HighlightsSubScreen( - sportList = sportList, + sportList = sportSelectionList, recentSearchList = recentSearchList, highlightsList = highlightsList, query = "s", diff --git a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt index 65b7da5..ecdbbdf 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -74,7 +74,7 @@ fun HomeScreen( } is ApiResponse.Error -> { - ErrorState({ homeViewModel.onRefresh() }, "Oops! Schedules failed to load.") + ErrorState({homeViewModel.onRefresh() }, "Oops! Schedules failed to load.") } is ApiResponse.Success -> { From 7832187eae79b3160d2a02ef727c040bd02e66eb Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 17:06:32 -0500 Subject: [PATCH 03/10] graphql files --- app/src/main/graphql/Highlights.graphql | 17 +++++++++++++++++ app/src/main/graphql/schema.graphqls | 17 ++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/src/main/graphql/Highlights.graphql diff --git a/app/src/main/graphql/Highlights.graphql b/app/src/main/graphql/Highlights.graphql new file mode 100644 index 0000000..fa8b5d4 --- /dev/null +++ b/app/src/main/graphql/Highlights.graphql @@ -0,0 +1,17 @@ +query Highlights($sportsType: String) { + articles(sportsType: $sportsType) { + title + image + sportsType + publishedAt + url + } + youtubeVideos { + title + thumbnail + url + publishedAt + duration + sportsType + } +} \ No newline at end of file diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls index e1ebfad..7ccb1bb 100644 --- a/app/src/main/graphql/schema.graphqls +++ b/app/src/main/graphql/schema.graphqls @@ -17,6 +17,8 @@ type Query { gamesBySportGender(sport: String!, gender: String!): [GameType] + gamesByDate(startDate: DateTime!, endDate: DateTime!): [GameType] + teams: [TeamType] team(id: String!): TeamType @@ -58,6 +60,8 @@ Attributes: - thumbnail: The URL of the video's thumbnail. - url: The URL to the video. - published_at: The date and time the video was published. + - duration: The duration of the video (optional). + - sportsType: The sport type extracted from the video title. """ type YoutubeVideoType { id: String @@ -73,6 +77,10 @@ type YoutubeVideoType { url: String! publishedAt: String! + + duration: String + + sportsType: String } """ @@ -181,6 +189,13 @@ type TeamType { name: String! } +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + type Mutation { """ Creates a new game. @@ -195,7 +210,7 @@ type Mutation { """ Creates a new youtube video. """ - createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + createYoutubeVideo(b64Thumbnail: String!, description: String!, duration: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo """ Creates a new article. From b439f225ffe20d5ba524f3b4fba362ac2403ac02 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 17:06:47 -0500 Subject: [PATCH 04/10] highlights models --- .../cornellappdev/score/model/Highlights.kt | 52 +++++++++++++++- .../score/model/HighlightsRepository.kt | 61 +++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt diff --git a/app/src/main/java/com/cornellappdev/score/model/Highlights.kt b/app/src/main/java/com/cornellappdev/score/model/Highlights.kt index a1a96b2..af95bc5 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Highlights.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Highlights.kt @@ -1,12 +1,16 @@ package com.cornellappdev.score.model +import com.cornellappdev.score.util.isValidSport +import com.example.score.HighlightsQuery + data class VideoHighlightData( val title: String, val thumbnailImageUrl: String, val videoUrl: String, val date: String, - val sport: Sport, - val gender: GenderDivision + val sport: Sport?, + val gender: GenderDivision?, + val duration: String? ) data class ArticleHighlightData( @@ -14,17 +18,59 @@ data class ArticleHighlightData( val imageUrl: String, val articleUrl: String, val date: String, - val sport: Sport + val sport: Sport? ) sealed class HighlightData { abstract val title: String + abstract val date: String + abstract val sport: Sport? data class Video(val data: VideoHighlightData) : HighlightData() { override val title = data.title + override val date = data.date + override val sport = data.sport } data class Article(val data: ArticleHighlightData) : HighlightData() { override val title = data.title + override val date = data.date + override val sport = data.sport } +} + +fun HighlightsQuery.YoutubeVideo.toHighlightData(): HighlightData? { + val sportName = sportsType ?: return null + if (!isValidSport(sportName)) return null + + return HighlightData.Video( + VideoHighlightData( + title = title, + thumbnailImageUrl = thumbnail, + videoUrl = url, + date = publishedAt, + sport = Sport.fromDisplayName(sportName), + gender = if (title.contains("Men's")) { + GenderDivision.MALE + } else { + GenderDivision.FEMALE + }, + duration = duration + ) + ) +} + +fun HighlightsQuery.Article.toHighlightData(): HighlightData? { + val sportName = sportsType + if (!isValidSport(sportName)) return null + + return HighlightData.Article( + ArticleHighlightData( + title = title, + imageUrl = image ?: return null, + articleUrl = url, + date = publishedAt, + sport = Sport.fromDisplayName(sportName) + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt b/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt new file mode 100644 index 0000000..8fb66a9 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt @@ -0,0 +1,61 @@ +package com.cornellappdev.score.model + +import android.util.Log +import com.apollographql.apollo.ApolloClient +import com.cornellappdev.score.util.isValidSport +import com.example.score.HighlightsQuery +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Singleton + +private const val TIMEOUT_TIME_MILLIS = 5000L +private const val PAGE_LIMIT = 100 +private const val MAX_RETRIES = 3 +private const val PAGE_TIMEOUT_MILLIS = 3000L + +@Singleton +class HighlightsRepository @Inject constructor( + private val apolloClient: ApolloClient, + private val appScope: CoroutineScope, +) { + private val _highlightsFlow = + MutableStateFlow>>(ApiResponse.Loading) + val highlightsFlow = _highlightsFlow.asStateFlow() + + + /** + * Asynchronously fetches the list of games from the API. Once finished, will send down + * `upcomingGamesFlow` to be observed. + */ + fun fetchHighlights() = appScope.launch { + _highlightsFlow.value = ApiResponse.Loading + try { + val result = + withTimeout(TIMEOUT_TIME_MILLIS) { + apolloClient.query(HighlightsQuery()).execute().toResult() + } + + if (result.isSuccess) { + val highlights = result.getOrNull() + val highlightsList = + highlights?.articles.orEmpty().mapNotNull { it?.toHighlightData() } + + highlights?.youtubeVideos.orEmpty().mapNotNull { it?.toHighlightData() } + + _highlightsFlow.value = + ApiResponse.Success(highlightsList) + + } else { + _highlightsFlow.value = ApiResponse.Error + } + + } catch (e: Exception) { + Log.e("HighlightsRepository", "Error fetching posts: ", e) + _highlightsFlow.value = ApiResponse.Error + } + } + +} \ No newline at end of file From 95a4f8823d6d0462b37566380713a52c47300740 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 17:07:17 -0500 Subject: [PATCH 05/10] added ISO date parsing function --- .../com/cornellappdev/score/util/DateUtil.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt index 3075fe7..265b0f1 100644 --- a/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt +++ b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt @@ -2,8 +2,10 @@ package com.cornellappdev.score.util import android.util.Log import java.time.Duration +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale @@ -66,6 +68,21 @@ fun parseDateTimeOrNull(date: String, time: String): LocalDateTime? { } } +/** + * Parses an ISO-8601 timestamp: yyyy-MM-dd'T'HH:mm:ss'Z' into a LocalDateTime object. + * + * @param strDate the date string to parse, in the format "yyyy-MM-dd'T'HH:mm:ss'Z'" + * @return a LocalDateTime object if parsing succeeds, or null if the format is invalid + */ +fun parseIsoDateToLocalDateOrNull(strDate: String): LocalDate? { + return try { + Instant.parse(strDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + } catch (e: Exception) { + null + } +} /** * Formats a date and time string into a user-friendly display string. From a67099b098b238c7cf81b57651c43620b20b1dc8 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 21:37:34 -0500 Subject: [PATCH 06/10] refactor sports type --- .../highlights/VideoHighlightsCard.kt | 10 ++- .../cornellappdev/score/nav/ScoreNavHost.kt | 3 +- .../score/screen/HighlightsScreen.kt | 72 +++++++++++++++---- .../score/util/TestingConstants.kt | 9 ++- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt index d1bc359..5b1f044 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt @@ -52,7 +52,9 @@ private fun VideoHighlightCardHeader( AsyncImage( model = imageUrl, contentDescription = "Highlight article image", - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() ) Box( modifier = Modifier @@ -163,7 +165,8 @@ class VideoHighlightsPreviewProvider : PreviewParameterProvider { backStackEntry -> CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) { HighlightsSearchScreen( - sportList = sportList, + sportList = sportSelectionList, recentSearchList = recentSearchList, highlightsList = highlightsList, query = "", diff --git a/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt index c3e30b0..16e5ac6 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt @@ -14,31 +14,75 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.R import com.cornellappdev.score.components.EmptyStateBox +import com.cornellappdev.score.components.ErrorState +import com.cornellappdev.score.components.LoadingScreen import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.components.ScorePullToRefreshBox import com.cornellappdev.score.components.highlights.HighlightsCardRow import com.cornellappdev.score.components.highlights.HighlightsFilterRow import com.cornellappdev.score.components.highlights.HighlightsSearchEntryPointRow +import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.HighlightData -import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.heading1 import com.cornellappdev.score.util.highlightsList -import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList +import com.cornellappdev.score.viewmodel.HighlightsViewModel @Composable fun HighlightsScreen( - sportList: List = emptyList(), //note - emptyLists are placeholders for nav to work, will replace will viewModel - todayHighlightsList: List = emptyList(), - pastThreeHighlightsList: List = emptyList(), + highlightsViewModel: HighlightsViewModel = hiltViewModel(), toSearchScreen: () -> Unit ) { + val uiState = highlightsViewModel.collectUiStateValue() + Column( modifier = Modifier .fillMaxSize() .background(color = Color.White) + .padding(top = 24.dp) + ) { + when (uiState.loadedState) { + is ApiResponse.Loading -> { + LoadingScreen("Loading Upcoming...", "Loading Schedules...") + } + + is ApiResponse.Error -> { + ErrorState({ highlightsViewModel.onRefresh() }, "Oops! Highlights failed to load.") + } + + is ApiResponse.Success -> { + ScorePullToRefreshBox( + isRefreshing = uiState.loadedState == ApiResponse.Loading, + { highlightsViewModel.onRefresh() } + ) { + HighlightsScreenContent( + sportList = uiState.sportSelectionList, + onSportSelected = { highlightsViewModel.onSportSelected(it) }, + todayHighlightsList = uiState.todayHighlights, + pastThreeHighlightsList = uiState.pastThreeDaysHighlights, + toSearchScreen = toSearchScreen + ) + } + } + } + } +} + +@Composable +private fun HighlightsScreenContent( + sportList: List = emptyList(), + onSportSelected: (SportSelection) -> Unit, + todayHighlightsList: List = emptyList(), + pastThreeHighlightsList: List = emptyList(), + toSearchScreen: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(24.dp)) Column( modifier = Modifier.padding(horizontal = 24.dp) ) { @@ -47,7 +91,7 @@ fun HighlightsScreen( HighlightsSearchEntryPointRow(toSearchScreen) } Spacer(modifier = Modifier.height(16.dp)) - HighlightsFilterRow(sportList, { /*todo: handle with viewmodel*/ }) + HighlightsFilterRow(sportList, onSportSelected) Spacer(modifier = Modifier.height(24.dp)) if (todayHighlightsList.isEmpty() && pastThreeHighlightsList.isEmpty()) { EmptyStateBox( @@ -64,17 +108,18 @@ fun HighlightsScreen( } } + data class HighlightsScreenPreviewData( - val sportList: List, + val sportList: List, val todayHighlightList: List, val pastHighlightList: List ) class HighlightsScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequence { - yield(HighlightsScreenPreviewData(sportList, highlightsList, highlightsList)) - yield(HighlightsScreenPreviewData(sportList, emptyList(), emptyList())) - yield(HighlightsScreenPreviewData(sportList, emptyList(), highlightsList)) + yield(HighlightsScreenPreviewData(sportSelectionList, highlightsList, highlightsList)) + yield(HighlightsScreenPreviewData(sportSelectionList, emptyList(), emptyList())) + yield(HighlightsScreenPreviewData(sportSelectionList, emptyList(), highlightsList)) } } @@ -84,11 +129,12 @@ private fun HighlightScreenPreview( @PreviewParameter(HighlightsScreenPreviewProvider::class) previewData: HighlightsScreenPreviewData ) { ScorePreview { - HighlightsScreen( + HighlightsScreenContent( sportList = previewData.sportList, todayHighlightsList = previewData.todayHighlightList, pastThreeHighlightsList = previewData.pastHighlightList, - toSearchScreen = {} + toSearchScreen = {}, + onSportSelected = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt index 476077b..9b31384 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -236,7 +236,8 @@ val highlightsList = listOf( "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", "11/09", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )), HighlightData.Article (ArticleHighlightData( @@ -253,7 +254,8 @@ val highlightsList = listOf( "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", "11/9", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )), HighlightData.Article (ArticleHighlightData( @@ -270,6 +272,7 @@ val highlightsList = listOf( "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", "11/9", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )) ) \ No newline at end of file From 90a2599b2c8d3db69a3a75b7796d4ed9e520f76b Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 21:37:47 -0500 Subject: [PATCH 07/10] highlights viewmodel --- .../score/viewmodel/HighlightsViewModel.kt | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt new file mode 100644 index 0000000..2cf402a --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt @@ -0,0 +1,103 @@ +package com.cornellappdev.score.viewmodel + +import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.HighlightData +import com.cornellappdev.score.model.HighlightsRepository +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection +import com.cornellappdev.score.model.map +import com.cornellappdev.score.util.parseIsoDateToLocalDateOrNull +import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDate +import javax.inject.Inject + +data class HighlightsUiState( + val sportSelect: SportSelection, + val loadedState: ApiResponse>, + val sportSelectionList: List +) { + //TODO: refactor filters to use flows - not best practice to expose original games list to the view + val filteredHighlights: List + get() = when (loadedState) { + is ApiResponse.Success -> loadedState.data.filter { highlight -> + (sportSelect is SportSelection.All || + (sportSelect is SportSelection.SportSelect && highlight.sport == sportSelect.sport)) + } + + ApiResponse.Loading -> emptyList() + ApiResponse.Error -> emptyList() + }.sortedBy { it.date } + + val todayHighlights: List + get() = when (loadedState) { + is ApiResponse.Success -> loadedState.data + + ApiResponse.Loading -> emptyList() + ApiResponse.Error -> emptyList() + }.filter { highlight -> + parseIsoDateToLocalDateOrNull(highlight.date) == LocalDate.now() + }.sortedBy { highlight -> + parseIsoDateToLocalDateOrNull(highlight.date) + } + + val pastThreeDaysHighlights: List + get() = when (loadedState) { + is ApiResponse.Success -> loadedState.data + + ApiResponse.Loading -> emptyList() + ApiResponse.Error -> emptyList() + }.filter { highlight -> + val date = parseIsoDateToLocalDateOrNull(highlight.date) + date != null && !date.isBefore(LocalDate.now().minusDays(3)) + }.sortedBy { highlight -> + parseIsoDateToLocalDateOrNull(highlight.date) + } + +} + +@HiltViewModel +class HighlightsViewModel @Inject constructor( + private val highlightsRepository: HighlightsRepository +) : BaseViewModel( + HighlightsUiState( + sportSelect = SportSelection.All, + loadedState = ApiResponse.Loading, + sportSelectionList = Sport.getSportSelectionList(GenderDivision.ALL) + ) +) { + init { + highlightsRepository.fetchHighlights() + asyncCollect(highlightsRepository.highlightsFlow) { response -> + applyMutation { + copy( + loadedState = response.map { highlights -> + highlights + .sortedByDescending { highlight -> + when (highlight) { + is HighlightData.Video -> highlight.data.date + is HighlightData.Article -> highlight.data.date + } + } + } + ) + } + } + } + + fun onRefresh() { + applyMutation { + copy(loadedState = ApiResponse.Loading) + } + + highlightsRepository.fetchHighlights() + } + + fun onSportSelected(sport: SportSelection) { + applyMutation { + copy( + sportSelect = sport + ) + } + } +} From bb2ed84790e80a1a610e668c77a0f88f70f4649c Mon Sep 17 00:00:00 2001 From: amjiao Date: Sat, 31 Jan 2026 22:24:52 -0500 Subject: [PATCH 08/10] Style fixes --- .../components/highlights/HighlightsSearchBar.kt | 3 +-- .../components/highlights/VideoHighlightsCard.kt | 14 ++++++++++---- .../score/model/HighlightsRepository.kt | 4 ---- .../com/cornellappdev/score/nav/ScoreNavHost.kt | 1 + .../cornellappdev/score/screen/HighlightsScreen.kt | 3 ++- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt index 3e47dba..6d6cae7 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/HighlightsSearchBar.kt @@ -95,14 +95,13 @@ fun HighlightsSearchBar( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( painter = painterResource(R.drawable.search), contentDescription = "search icon", tint = Color.Unspecified ) - Spacer(Modifier.width(4.dp)) Box { innerTextField() if (searchQuery.isEmpty()) { diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt index 5b1f044..a71f2f5 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt @@ -96,17 +96,23 @@ fun VideoHighlightCardBody( Row( verticalAlignment = Alignment.CenterVertically ) { - if (videoHighlight.sport != null) { + videoHighlight.sport?.let { sport -> Icon( - painter = painterResource(videoHighlight.sport.emptyIcon), + painter = painterResource(sport.emptyIcon), contentDescription = "Sport icon", modifier = Modifier.size(24.dp), tint = Color.Unspecified ) } - if (videoHighlight.gender != null) { + + videoHighlight.gender?.let { gender -> + val iconRes = when (gender) { + GenderDivision.FEMALE -> R.drawable.ic_gender_women + else -> R.drawable.ic_gender_men + } + Icon( - painter = painterResource(if (videoHighlight.gender == GenderDivision.FEMALE) R.drawable.ic_gender_women else R.drawable.ic_gender_men), + painter = painterResource(iconRes), contentDescription = "Gender icon", tint = Color.Unspecified ) diff --git a/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt b/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt index 8fb66a9..f97175f 100644 --- a/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/HighlightsRepository.kt @@ -2,7 +2,6 @@ package com.cornellappdev.score.model import android.util.Log import com.apollographql.apollo.ApolloClient -import com.cornellappdev.score.util.isValidSport import com.example.score.HighlightsQuery import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -13,9 +12,6 @@ import javax.inject.Inject import javax.inject.Singleton private const val TIMEOUT_TIME_MILLIS = 5000L -private const val PAGE_LIMIT = 100 -private const val MAX_RETRIES = 3 -private const val PAGE_TIMEOUT_MILLIS = 3000L @Singleton class HighlightsRepository @Inject constructor( diff --git a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt index 7a86013..b0e916b 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt @@ -78,6 +78,7 @@ fun ScoreNavHost(navController: NavHostController) { composable { backStackEntry -> CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) { HighlightsSearchScreen( + //todo - will un-hardcode this when i do the networking sportList = sportSelectionList, recentSearchList = recentSearchList, highlightsList = highlightsList, diff --git a/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt index 16e5ac6..8d52592 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HighlightsScreen.kt @@ -47,7 +47,8 @@ fun HighlightsScreen( ) { when (uiState.loadedState) { is ApiResponse.Loading -> { - LoadingScreen("Loading Upcoming...", "Loading Schedules...") + //todo make highlights loading screen, this one's for the home page + LoadingScreen("Loading Highlights...", "Loading Schedules...") } is ApiResponse.Error -> { From 5d1fb1d27f0d6a544ee00cfe539f10854e33f086 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 1 Feb 2026 00:00:37 -0500 Subject: [PATCH 09/10] Add dateString field to HighlightsData type --- .../highlights/ArticleHighlightsCard.kt | 4 ++- .../highlights/VideoHighlightsCard.kt | 8 +++--- .../cornellappdev/score/model/Highlights.kt | 27 +++++++++++++++---- .../score/util/TestingConstants.kt | 5 ++++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/ArticleHighlightsCard.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/ArticleHighlightsCard.kt index bc34227..30d2630 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/ArticleHighlightsCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/ArticleHighlightsCard.kt @@ -95,7 +95,7 @@ fun ArticleHighlightCard( Text( color = Color.White, style = labelsNormal, - text = articleHighlight.date + text = articleHighlight.dateString ) } } @@ -110,6 +110,7 @@ private fun ArticleHighlightCardPreview() { "Late Goal Lifts No. 6 Men’s Hockey Over Brown", "maxresdefault.jpg", "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", + null, "11/9", Sport.ICE_HOCKEY ), @@ -125,6 +126,7 @@ private fun WideArticleHighlightCardPreview() { "Late Goal Lifts No. 6 Men’s Hockey Over Brown", "maxresdefault.jpg", "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", + null, "11/9", Sport.ICE_HOCKEY ), diff --git a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt index a71f2f5..a4f3714 100644 --- a/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/highlights/VideoHighlightsCard.kt @@ -135,7 +135,7 @@ fun VideoHighlightCardBody( } Text( style = labelsNormal, - text = videoHighlight.date + text = videoHighlight.dateString ) } } @@ -169,7 +169,8 @@ class VideoHighlightsPreviewProvider : PreviewParameterProvider Date: Sun, 1 Feb 2026 00:06:05 -0500 Subject: [PATCH 10/10] Refactor HighlightsViewModel UIState no longer exposes the raw backend list. Filtered highlights are now precomputed in the ViewModel, so the UI can observe state directly without recalculating on every recomposition. --- .../score/viewmodel/HighlightsViewModel.kt | 123 +++++++++++------- 1 file changed, 75 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt index 2cf402a..280b67b 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt @@ -6,8 +6,6 @@ import com.cornellappdev.score.model.HighlightData import com.cornellappdev.score.model.HighlightsRepository import com.cornellappdev.score.model.Sport import com.cornellappdev.score.model.SportSelection -import com.cornellappdev.score.model.map -import com.cornellappdev.score.util.parseIsoDateToLocalDateOrNull import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate import javax.inject.Inject @@ -15,45 +13,40 @@ import javax.inject.Inject data class HighlightsUiState( val sportSelect: SportSelection, val loadedState: ApiResponse>, - val sportSelectionList: List -) { - //TODO: refactor filters to use flows - not best practice to expose original games list to the view - val filteredHighlights: List - get() = when (loadedState) { - is ApiResponse.Success -> loadedState.data.filter { highlight -> - (sportSelect is SportSelection.All || - (sportSelect is SportSelection.SportSelect && highlight.sport == sportSelect.sport)) - } + val sportSelectionList: List, + val filteredHighlights: List, + val todayHighlights: List, + val pastThreeDaysHighlights: List +) - ApiResponse.Loading -> emptyList() - ApiResponse.Error -> emptyList() - }.sortedBy { it.date } +private fun buildDerivedLists( + highlights: List, + sportSelect: SportSelection +): Triple, List, List> { - val todayHighlights: List - get() = when (loadedState) { - is ApiResponse.Success -> loadedState.data + val today = LocalDate.now() + val threeDaysAgo = today.minusDays(3) - ApiResponse.Loading -> emptyList() - ApiResponse.Error -> emptyList() - }.filter { highlight -> - parseIsoDateToLocalDateOrNull(highlight.date) == LocalDate.now() - }.sortedBy { highlight -> - parseIsoDateToLocalDateOrNull(highlight.date) + // Keep only highlights with a date and matching the sport filter + val validHighlights = highlights + .filter { it.date != null } + .filter { highlight -> + when (sportSelect) { + is SportSelection.All -> true + is SportSelection.SportSelect -> + highlight.sport == sportSelect.sport + } } - val pastThreeDaysHighlights: List - get() = when (loadedState) { - is ApiResponse.Success -> loadedState.data - - ApiResponse.Loading -> emptyList() - ApiResponse.Error -> emptyList() - }.filter { highlight -> - val date = parseIsoDateToLocalDateOrNull(highlight.date) - date != null && !date.isBefore(LocalDate.now().minusDays(3)) - }.sortedBy { highlight -> - parseIsoDateToLocalDateOrNull(highlight.date) - } + val filtered = validHighlights.sortedBy { it.date } + + val todayHighlights = validHighlights.filter { it.date == today } + + val pastThreeDaysHighlights = validHighlights + .filter { it.date!! >= threeDaysAgo } // null dates filtered out in line 32 + .sortedBy { it.date } + return Triple(filtered, todayHighlights, pastThreeDaysHighlights) } @HiltViewModel @@ -63,24 +56,48 @@ class HighlightsViewModel @Inject constructor( HighlightsUiState( sportSelect = SportSelection.All, loadedState = ApiResponse.Loading, - sportSelectionList = Sport.getSportSelectionList(GenderDivision.ALL) + sportSelectionList = Sport.getSportSelectionList(GenderDivision.ALL), + filteredHighlights = emptyList(), + todayHighlights = emptyList(), + pastThreeDaysHighlights = emptyList() ) ) { init { highlightsRepository.fetchHighlights() asyncCollect(highlightsRepository.highlightsFlow) { response -> applyMutation { - copy( - loadedState = response.map { highlights -> - highlights - .sortedByDescending { highlight -> - when (highlight) { - is HighlightData.Video -> highlight.data.date - is HighlightData.Article -> highlight.data.date - } - } + when (response) { + is ApiResponse.Success -> { + val sorted = + response.data.sortedByDescending { it.date } + + val (filtered, today, pastThreeDays) = + buildDerivedLists(sorted, sportSelect) + + copy( + loadedState = ApiResponse.Success(sorted), + filteredHighlights = filtered, + todayHighlights = today, + pastThreeDaysHighlights = pastThreeDays + ) } - ) + + ApiResponse.Loading -> + copy( + loadedState = ApiResponse.Loading, + filteredHighlights = emptyList(), + todayHighlights = emptyList(), + pastThreeDaysHighlights = emptyList() + ) + + ApiResponse.Error -> + copy( + loadedState = ApiResponse.Error, + filteredHighlights = emptyList(), + todayHighlights = emptyList(), + pastThreeDaysHighlights = emptyList() + ) + } } } } @@ -89,14 +106,24 @@ class HighlightsViewModel @Inject constructor( applyMutation { copy(loadedState = ApiResponse.Loading) } - highlightsRepository.fetchHighlights() } fun onSportSelected(sport: SportSelection) { applyMutation { + val highlights = when (val state = loadedState) { + is ApiResponse.Success -> state.data + else -> emptyList() + } + + val (filtered, today, pastThreeDays) = + buildDerivedLists(highlights, sport) + copy( - sportSelect = sport + sportSelect = sport, + filteredHighlights = filtered, + todayHighlights = today, + pastThreeDaysHighlights = pastThreeDays ) } }