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. 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/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..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 @@ -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,21 +89,19 @@ fun HighlightsSearchBar( Row( modifier = Modifier - .highlightsSearchRowModifier() - .padding(horizontal = 8.dp), + .highlightsSearchRowModifier(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { 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(8.dp)) Box { innerTextField() if (searchQuery.isEmpty()) { @@ -162,7 +160,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..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 @@ -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 @@ -94,17 +96,27 @@ 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 - ) + videoHighlight.sport?.let { sport -> + Icon( + painter = painterResource(sport.emptyIcon), + contentDescription = "Sport icon", + modifier = Modifier.size(24.dp), + tint = Color.Unspecified + ) + } + + videoHighlight.gender?.let { gender -> + val iconRes = when (gender) { + GenderDivision.FEMALE -> R.drawable.ic_gender_women + else -> R.drawable.ic_gender_men + } + + Icon( + painter = painterResource(iconRes), + contentDescription = "Gender icon", + tint = Color.Unspecified + ) + } } } Spacer(Modifier.height(8.dp)) @@ -123,7 +135,7 @@ fun VideoHighlightCardBody( } Text( style = labelsNormal, - text = videoHighlight.date + text = videoHighlight.dateString ) } } @@ -157,17 +169,21 @@ class VideoHighlightsPreviewProvider : PreviewParameterProvider>>(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 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 7002312..b0e916b 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt @@ -19,6 +19,7 @@ import com.cornellappdev.score.screen.PastGamesScreen import com.cornellappdev.score.util.highlightsList import com.cornellappdev.score.util.recentSearchList import com.cornellappdev.score.util.sportList +import com.cornellappdev.score.util.sportSelectionList import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json @@ -77,7 +78,8 @@ fun ScoreNavHost(navController: NavHostController) { composable { backStackEntry -> CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) { HighlightsSearchScreen( - sportList = sportList, + //todo - will un-hardcode this when i do the networking + sportList = sportSelectionList, recentSearchList = recentSearchList, highlightsList = highlightsList, query = "", 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") } 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..8d52592 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,76 @@ 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 -> { + //todo make highlights loading screen, this one's for the home page + LoadingScreen("Loading Highlights...", "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 +92,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 +109,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 +130,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/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 -> { 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. 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..8018e54 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -234,15 +234,18 @@ val highlightsList = listOf( "vs Columbia", "maxresdefault.jpg", "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", + null, "11/09", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )), HighlightData.Article (ArticleHighlightData( "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/09", Sport.ICE_HOCKEY )), @@ -251,15 +254,18 @@ val highlightsList = listOf( "vs Columbia", "maxresdefault.jpg", "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", + null, "11/9", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )), HighlightData.Article (ArticleHighlightData( "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/09", Sport.ICE_HOCKEY )), @@ -268,8 +274,10 @@ val highlightsList = listOf( "vs Columbia", "maxresdefault.jpg", "https://cornellsun.com/article/london-mcdavid-is-making-a-name-for-herself-at-cornell", + null, "11/9", Sport.BASEBALL, - GenderDivision.MALE + GenderDivision.MALE, + "0:44" )) ) \ No newline at end of file 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..280b67b --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/HighlightsViewModel.kt @@ -0,0 +1,130 @@ +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 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, + val filteredHighlights: List, + val todayHighlights: List, + val pastThreeDaysHighlights: List +) + +private fun buildDerivedLists( + highlights: List, + sportSelect: SportSelection +): Triple, List, List> { + + val today = LocalDate.now() + val threeDaysAgo = today.minusDays(3) + + // 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 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 +class HighlightsViewModel @Inject constructor( + private val highlightsRepository: HighlightsRepository +) : BaseViewModel( + HighlightsUiState( + sportSelect = SportSelection.All, + loadedState = ApiResponse.Loading, + sportSelectionList = Sport.getSportSelectionList(GenderDivision.ALL), + filteredHighlights = emptyList(), + todayHighlights = emptyList(), + pastThreeDaysHighlights = emptyList() + ) +) { + init { + highlightsRepository.fetchHighlights() + asyncCollect(highlightsRepository.highlightsFlow) { response -> + applyMutation { + 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() + ) + } + } + } + } + + fun onRefresh() { + 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, + filteredHighlights = filtered, + todayHighlights = today, + pastThreeDaysHighlights = pastThreeDays + ) + } + } +}