From 97882f1299af9576b1eddb0dd7afcde83bfbf957 Mon Sep 17 00:00:00 2001 From: helenjb Date: Tue, 18 Mar 2025 14:15:02 -0400 Subject: [PATCH 1/5] Implement navigation and past games screen UI/ViewModel/Networking --- ...pcomingGameCard.kt => FeaturedGameCard.kt} | 63 ++++- .../components/{SportCard.kt => GameCard.kt} | 17 +- ...omingGamesCarousel.kt => GamesCarousel.kt} | 11 +- .../score/components/PastGameCard.kt | 231 ++++++++++++++++++ .../com/cornellappdev/score/model/Game.kt | 4 +- .../com/cornellappdev/score/model/Sport.kt | 4 +- .../score/nav/root/RootNavigation.kt | 114 ++++++++- .../score/screen/GameDetailsScreen.kt | 4 +- .../cornellappdev/score/screen/HomeScreen.kt | 17 +- .../score/screen/PastGamesScreen.kt | 89 +++++++ .../cornellappdev/score/theme/TextStyle.kt | 38 ++- .../score/viewmodel/PastGamesViewModel.kt | 163 ++++++++++++ app/src/main/res/drawable/ic_arrow_back.xml | 10 + app/src/main/res/drawable/ic_baseball.xml | 15 +- app/src/main/res/drawable/ic_schedule.xml | 12 + .../main/res/drawable/ic_schedule_filled.xml | 11 + app/src/main/res/drawable/ic_scores.xml | 10 + .../main/res/drawable/ic_scores_filled.xml | 9 + 18 files changed, 761 insertions(+), 61 deletions(-) rename app/src/main/java/com/cornellappdev/score/components/{UpcomingGameCard.kt => FeaturedGameCard.kt} (68%) rename app/src/main/java/com/cornellappdev/score/components/{SportCard.kt => GameCard.kt} (96%) rename app/src/main/java/com/cornellappdev/score/components/{UpcomingGamesCarousel.kt => GamesCarousel.kt} (93%) create mode 100644 app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt create mode 100644 app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml create mode 100644 app/src/main/res/drawable/ic_schedule.xml create mode 100644 app/src/main/res/drawable/ic_schedule_filled.xml create mode 100644 app/src/main/res/drawable/ic_scores.xml create mode 100644 app/src/main/res/drawable/ic_scores_filled.xml diff --git a/app/src/main/java/com/cornellappdev/score/components/UpcomingGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt similarity index 68% rename from app/src/main/java/com/cornellappdev/score/components/UpcomingGameCard.kt rename to app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt index 287e5a4..e782cee 100644 --- a/app/src/main/java/com/cornellappdev/score/components/UpcomingGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt @@ -10,6 +10,8 @@ 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.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,22 +23,30 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.cornellappdev.score.R import com.cornellappdev.score.theme.CornellRed import com.cornellappdev.score.theme.PennBlue +import com.cornellappdev.score.theme.Style.losingScoreText import com.cornellappdev.score.theme.Style.vsText +import com.cornellappdev.score.theme.Style.winningScoreText @Composable -fun UpcomingGameHeader( +fun FeaturedGameHeader( leftTeamLogo: Painter, rightTeamLogo: String, + leftScore: Int? = null, + rightScore: Int? = null, gradientColor1: Color, gradientColor2: Color, modifier: Modifier = Modifier ) { + val isPast = (leftScore != null && rightScore != null) + Box( modifier = modifier .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) @@ -49,7 +59,7 @@ fun UpcomingGameHeader( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), + horizontalArrangement = if(isPast) Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally) else Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), modifier = Modifier .fillMaxWidth() .padding(vertical = 36.dp) @@ -60,11 +70,32 @@ fun UpcomingGameHeader( contentScale = ContentScale.FillBounds, modifier = Modifier.size(60.dp) ) - - Text( - text = "VS", - style = vsText - ) + if(leftScore != null && rightScore != null) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = leftScore.toString(), + style = if(leftScore > rightScore) winningScoreText else losingScoreText, + modifier = Modifier.width(52.dp) + .wrapContentWidth(Alignment.CenterHorizontally) + ) + Text( + text = "-", + style = vsText.copy(fontSize = 32.sp, fontStyle = FontStyle.Normal) + ) + Text( + text = rightScore.toString(), + style = if(leftScore < rightScore) winningScoreText else losingScoreText, + modifier = Modifier.width(52.dp) + .wrapContentWidth(Alignment.CenterHorizontally) + ) + } + } + else { + Text( + text = "VS", + style = vsText + ) + } AsyncImage( model = rightTeamLogo, contentDescription = "Right Team Logo", @@ -76,8 +107,8 @@ fun UpcomingGameHeader( @Preview @Composable -fun UpcomingGameHeaderPreview() { - UpcomingGameHeader( +private fun FeaturedGameCardPreview() { + FeaturedGameHeader( leftTeamLogo = painterResource(R.drawable.cornell_logo), rightTeamLogo = "https://cornellbigred.com/images/logos/YALE_LOGO_2020.png?width=80&height=80&mode=max", gradientColor1 = CornellRed, @@ -87,9 +118,11 @@ fun UpcomingGameHeaderPreview() { } @Composable -fun UpcomingGameCard( +fun FeaturedGameCard( leftTeamLogo: Painter, rightTeamLogo: String, + leftScore: Int? = null, + rightScore: Int? = null, team: String, location: String, isLive: Boolean, @@ -106,15 +139,17 @@ fun UpcomingGameCard( .fillMaxWidth() ) { - UpcomingGameHeader( + FeaturedGameHeader( leftTeamLogo = leftTeamLogo, rightTeamLogo = rightTeamLogo, + leftScore = leftScore, + rightScore = rightScore, gradientColor1 = gradientColor1, gradientColor2 = gradientColor2, modifier = headerModifier ) - SportCard( + GameCard( teamLogo = rightTeamLogo, team = team, date = date, @@ -139,9 +174,11 @@ fun UpcomingGameCard( @Preview(showBackground = true) @Composable fun GameScheduleScreen() { - UpcomingGameCard( + FeaturedGameCard( leftTeamLogo = painterResource(R.drawable.cornell_logo), rightTeamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max",//painterResource(R.drawable.penn_logo), + leftScore = 32, + rightScore = 30, team = "Penn", location = "Philadelphia, NJ", date = "5/20/2024", diff --git a/app/src/main/java/com/cornellappdev/score/components/SportCard.kt b/app/src/main/java/com/cornellappdev/score/components/GameCard.kt similarity index 96% rename from app/src/main/java/com/cornellappdev/score/components/SportCard.kt rename to app/src/main/java/com/cornellappdev/score/components/GameCard.kt index 044bad7..25ff327 100644 --- a/app/src/main/java/com/cornellappdev/score/components/SportCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GameCard.kt @@ -1,6 +1,5 @@ package com.cornellappdev.score.components -import android.icu.text.SimpleDateFormat import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -9,6 +8,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -28,23 +28,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.cornellappdev.score.R +import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.theme.AmbientColor import com.cornellappdev.score.theme.GrayMedium import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.SpotColor import com.cornellappdev.score.theme.Style.bodyNormal -import com.cornellappdev.score.theme.Style.dateText import com.cornellappdev.score.theme.Style.heading2 import com.cornellappdev.score.theme.Style.labelsNormal -import com.cornellappdev.score.theme.Style.teamName -import com.cornellappdev.score.theme.Style.universityText import com.cornellappdev.score.theme.saturatedGreen -import java.util.Date -import java.util.Locale @Composable -fun SportCard( +fun GameCard( teamLogo: String, team: String, date: String, @@ -54,6 +50,7 @@ fun SportCard( sportIcon: Painter, topCornerRound: Boolean, modifier: Modifier = Modifier, + onClick: (Boolean) -> Unit = {} ) { val cardShape = if (topCornerRound) { RoundedCornerShape(16.dp) // Rounded all @@ -84,7 +81,7 @@ fun SportCard( ) ) } - ) + ).clickable { onClick(false) } ) { Column( modifier = Modifier @@ -199,9 +196,9 @@ fun SportCard( @Preview(showBackground = true) @Composable -fun SportCardPreview() { +private fun GameCardPreview() { Column { - SportCard( + GameCard( teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", //painterResource(id = R.drawable.penn_logo), team = "Penn", date = "5/20/2024", diff --git a/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt similarity index 93% rename from app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt rename to app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt index c160ed1..7406c50 100644 --- a/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt @@ -26,7 +26,6 @@ import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayLight import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.Style.heading1 -import com.cornellappdev.score.theme.Style.title import com.cornellappdev.score.util.gameList @Composable @@ -56,7 +55,7 @@ fun DotIndicator( } @Composable -fun UpcomingGamesCarousel(games: List) { +fun GamesCarousel(games: List, upcoming: Boolean) { val pagerState = rememberPagerState(pageCount = { games.size }) Column( modifier = Modifier @@ -65,7 +64,7 @@ fun UpcomingGamesCarousel(games: List) { verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), ) { Text( - text = "Upcoming", + text = if (upcoming) "Upcoming" else "Latest", style = heading1, color = GrayPrimary, modifier = Modifier.fillMaxWidth() @@ -76,7 +75,7 @@ fun UpcomingGamesCarousel(games: List) { modifier = Modifier.fillMaxWidth() ) { page -> val game = games[page] - UpcomingGameCard( + FeaturedGameCard( leftTeamLogo = painterResource(R.drawable.cornell_logo), rightTeamLogo = game.teamLogo, team = game.team, @@ -104,6 +103,6 @@ fun UpcomingGamesCarousel(games: List) { @Preview(showBackground = true, widthDp = 360) @Composable -private fun UpcomingGamesCarouselPreview() { - UpcomingGamesCarousel(gameList) +private fun GamesCarouselPreview() { + GamesCarousel(gameList, true) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt new file mode 100644 index 0000000..6fba7ad --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -0,0 +1,231 @@ +package com.cornellappdev.score.components + +import android.graphics.drawable.Icon +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.theme.AmbientColor +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.SpotColor +import com.cornellappdev.score.theme.Style.bodyNormal +import com.cornellappdev.score.theme.Style.heading2 +import com.cornellappdev.score.theme.Style.labelsNormal +import com.cornellappdev.score.theme.Style.metricMedium +import com.cornellappdev.score.theme.saturatedGreen + +@Composable +fun PastGameCard( + teamLogo: String, + team: String, + date: String, + location: String, + genderIcon: Painter, + sportIcon: Painter, + modifier: Modifier = Modifier, + cornellScore: Int, + otherScore: Int, + onClick: (Boolean) -> Unit = {} +) { + val cornellWins = cornellScore > otherScore + val isHome = location == "Ithaca, NY" + val firstWins = (cornellWins && isHome) || (!cornellWins && !isHome) + + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .shadow(elevation = 6.dp, spotColor = SpotColor, ambientColor = AmbientColor) + .then( + Modifier + .border(width = 1.dp, color = GrayStroke, RoundedCornerShape(16.dp)) + ) + .clickable { onClick(true) } + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier + .width(224.dp) + .drawBehind { + drawLine( + color = GrayLight, + start = Offset(size.width, 0f), + end = Offset(size.width, size.height), + strokeWidth = 1.dp.toPx() + ) + }) { + Row(horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth()) { + Row(modifier = Modifier.widthIn(0.dp, 170.dp)) { + if (isHome){ + Image( + painter = painterResource(R.drawable.cornell_logo), + contentDescription = "Cornell Logo", + modifier = Modifier.height(27.dp).padding(horizontal = 2.dp, vertical = 4.dp) + ) + } else { + AsyncImage( + model = teamLogo, + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + contentDescription = "" + ) + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if(isHome) "Cornell" else team, + style = heading2, + color = if(firstWins) GrayPrimary else GrayLight + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = if(isHome) cornellScore.toString() else otherScore.toString(), + style = metricMedium, + color = if(firstWins) GrayPrimary else GrayLight) + Spacer(modifier = Modifier.width(12.dp)) + if(firstWins) { + Icon(painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = "Indicates winning team", + modifier = Modifier + .width(11.dp) + .height(14.dp),) + } else { + Box(modifier = Modifier.width(11.dp)) + } + } + } + Spacer(modifier = Modifier.height(10.dp)) + Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.widthIn(0.dp, 170.dp)) { + if (!isHome){ + Image( + painter = painterResource(R.drawable.cornell_logo), + contentDescription = "Cornell Logo", + modifier = Modifier.height(27.dp).padding(horizontal = 4.dp, vertical = 4.dp) + ) + } else { + AsyncImage( + model = teamLogo, + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + contentDescription = "" + ) + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if(!isHome) "Cornell" else team, + style = heading2, + color = if(firstWins) GrayLight else GrayPrimary + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = if(!isHome) cornellScore.toString() else otherScore.toString(), + style = metricMedium, + color = if(firstWins) GrayLight else GrayPrimary) + Spacer(modifier = Modifier.width(12.dp)) + if(!firstWins) { + Icon(painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = "Indicates winning team", + modifier = Modifier + .width(11.dp) + .height(14.dp),) + } else { + Box(modifier = Modifier.width(11.dp)) + } + } + } + } + Spacer(modifier = Modifier.width(24.dp)) + Column(modifier = Modifier.height(64.dp), + verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.End) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = sportIcon, + contentDescription = "Sport Icon", + modifier = Modifier + .width(24.dp) + .height(24.dp), + tint = Color.Unspecified + ) + Icon( + painter = genderIcon, + contentDescription = "Gender Icon", + modifier = Modifier + .padding(2.5.dp) + .width(19.dp) + .height(19.dp), + tint = Color.Unspecified + ) + } + Text( + text = date, + style = labelsNormal, + color = GrayMedium + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PastGameCardPreview() { + Column { + PastGameCard( + teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", //painterResource(id = R.drawable.penn_logo), + team = "University of Pennsylvania", + date = "5/20/2024", + location = "U. Pennsylvania", + genderIcon = painterResource(id = R.drawable.ic_gender_men), + sportIcon = painterResource(id = R.drawable.ic_baseball), + modifier = Modifier.padding(16.dp), + cornellScore = 3, + otherScore = 0 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index 7a71627..1dadb71 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -27,7 +27,9 @@ data class GameCardData( val gender: String, val genderIcon: Int, val sport: String, - val sportIcon: Int + val sportIcon: Int, + val cornellScore: Int? = null, + val otherScore: Int? = null, ) // Scoring information for a specific team, used in the box score diff --git a/app/src/main/java/com/cornellappdev/score/model/Sport.kt b/app/src/main/java/com/cornellappdev/score/model/Sport.kt index 239b757..da7ff84 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Sport.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Sport.kt @@ -158,10 +158,10 @@ enum class GenderDivision( val displayName: String ) { FEMALE( - "Womens" + "Women's" ), MALE( - "Mens" + "Men's" ), ALL( "All" 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 e6db353..9cd4e93 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 @@ -2,14 +2,36 @@ package com.cornellappdev.score.nav.root import android.os.Build import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import com.cornellappdev.score.R +import com.cornellappdev.score.nav.root.ScoreRootScreens.Home.toScreen +import com.cornellappdev.score.screen.GameDetailsScreen import com.cornellappdev.score.screen.HomeScreen +import com.cornellappdev.score.screen.PastGamesScreen +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.Style.bodyMedium import kotlinx.serialization.Serializable +import java.time.LocalDate @RequiresApi(Build.VERSION_CODES.O) @Composable @@ -18,6 +40,7 @@ fun RootNavigation( ) { val navController = rememberNavController() val uiState = rootNavigationViewModel.collectUiStateValue() + val navBackStackEntry = navController.currentBackStackEntryAsState().value LaunchedEffect(uiState.navigationEvent) { uiState.navigationEvent?.consumeSuspend { screen -> @@ -25,20 +48,58 @@ fun RootNavigation( } } - NavHost( - navController = navController, - startDestination = ScoreRootScreens.Home - ) { - composable { - HomeScreen() - } - - composable { + Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = { + NavigationBar { + tabs.map { item -> + val isSelected = item.screen == navBackStackEntry?.toScreen() + NavigationBarItem( + selected = isSelected, + onClick = { navController.navigate(item.screen) }, + icon = { Icon(painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon), contentDescription = null, + tint = Color.Unspecified) } , + label = { Text(text = item.label, + style = bodyMedium, + color = if (isSelected) { + CrimsonPrimary + } else { + GrayPrimary + }) } + ) + } } + } + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + NavHost( + navController = navController, + startDestination = ScoreRootScreens.Home + ) { + composable { + HomeScreen(navigateToGameDetails = { isPast : Boolean -> + navController.navigate(ScoreRootScreens.GameDetailsPage("", isPast)) + }) + } - composable { + composable { navBackStackEntry -> + val isPast = navBackStackEntry.toRoute().isPast + if(isPast) { + GameDetailsScreen("", onBackArrow = { + navController.navigate(ScoreRootScreens.ScoresScreen) + }) + } else { + GameDetailsScreen("", onBackArrow = { + navController.navigate(ScoreRootScreens.Home) + }) + } + } + composable { + PastGamesScreen(navigateToGameDetails = { isPast : Boolean -> + navController.navigate(ScoreRootScreens.GameDetailsPage("", isPast)) + }) + } + } } } } @@ -50,9 +111,38 @@ sealed class ScoreRootScreens { data object Home : ScoreRootScreens() @Serializable - data object GameDetailPage : ScoreRootScreens() + data class GameDetailsPage(val gameId: String, val isPast: Boolean) : ScoreRootScreens() @Serializable - data object Onboarding : ScoreRootScreens() + data object ScoresScreen : ScoreRootScreens() + fun NavBackStackEntry.toScreen(): ScoreRootScreens? = + when (destination.route?.substringAfterLast(".")?.substringBefore("/")) { + "Home" -> toRoute() + "GameDetailsPage" -> toRoute() + "ScoresScreen" -> toRoute() + else -> null + } } + +data class NavItem( + val screen: ScoreRootScreens, + val label: String, + val unselectedIcon: Int, + val selectedIcon: Int +) + +val tabs = listOf( + NavItem( + label = "Schedule", + unselectedIcon = R.drawable.ic_schedule, + selectedIcon = R.drawable.ic_schedule_filled, + screen = ScoreRootScreens.Home, + ), + NavItem( + label = "Scores", + unselectedIcon = R.drawable.ic_scores, + selectedIcon = R.drawable.ic_scores_filled, + screen = ScoreRootScreens.ScoresScreen, + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt index e09ef76..da429dd 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -32,10 +32,10 @@ import com.cornellappdev.score.theme.Style.heading3 import com.cornellappdev.score.theme.White @Composable -fun GameDetailsScreen() { +fun GameDetailsScreen(gameId: String = "", onBackArrow: () -> Unit = {}) { Column(modifier = Modifier.background(White).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { // TODO: add navigation - NavigationHeader(title = "Game Details", {}) + NavigationHeader(title = "Game Details", onBackArrow) GameScoreHeader( leftTeamLogo = painterResource(R.drawable.cornell_logo), rightTeamLogo = painterResource(R.drawable.penn_logo), 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 8602d81..2b0940f 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -20,17 +20,17 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.cornellappdev.score.components.SportCard +import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.SportSelectorHeader -import com.cornellappdev.score.components.UpcomingGamesCarousel +import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.theme.Style.heading1 -import com.cornellappdev.score.theme.Style.title import com.cornellappdev.score.viewmodel.HomeViewModel @RequiresApi(Build.VERSION_CODES.O)//TODO - change the manifest or leave this? @Composable fun HomeScreen( - homeViewModel: HomeViewModel = hiltViewModel() + homeViewModel: HomeViewModel = hiltViewModel(), + navigateToGameDetails: (Boolean) -> Unit = {} ) { val uiState by homeViewModel.uiStateFlow.collectAsState() @@ -40,11 +40,11 @@ fun HomeScreen( ) { //TODO: check - displaying the earliest three games - UpcomingGamesCarousel( + GamesCarousel( uiState.upcomingGameList.subList( 0, minOf(3, uiState.upcomingGameList.size) - ) + ), true ) Column { Text( @@ -69,7 +69,7 @@ fun HomeScreen( LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { items(uiState.filteredGames.size) { page -> val game = uiState.filteredGames[page] - SportCard( + GameCard( teamLogo = game.teamLogo,//painterResource(game.teamLogo), team = game.team, date = game.dateString, @@ -77,7 +77,8 @@ fun HomeScreen( genderIcon = painterResource(game.genderIcon), sportIcon = painterResource(game.sportIcon), location = game.location, - topCornerRound = true + topCornerRound = true, + onClick = navigateToGameDetails ) } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt new file mode 100644 index 0000000..2604fa0 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -0,0 +1,89 @@ +package com.cornellappdev.score.screen + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.score.components.GameCard +import com.cornellappdev.score.components.SportSelectorHeader +import com.cornellappdev.score.components.GamesCarousel +import com.cornellappdev.score.components.PastGameCard +import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.viewmodel.HomeViewModel +import com.cornellappdev.score.viewmodel.PastGamesViewModel + +@RequiresApi(Build.VERSION_CODES.O)//TODO - change the manifest or leave this? +@Composable +fun PastGamesScreen( + pastGamesViewModel: PastGamesViewModel = hiltViewModel(), + navigateToGameDetails: (Boolean) -> Unit = {} +) { + val uiState by pastGamesViewModel.uiStateFlow.collectAsState() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), + modifier = Modifier.statusBarsPadding() + ) + { + //TODO: check - displaying the earliest three games + GamesCarousel( + uiState.pastGameList.subList( + 0, + minOf(3, uiState.pastGameList.size) + ), false + ) + Column { + Text( + text = "All Scores", + style = heading1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + SportSelectorHeader( + sports = uiState.selectionList, + selectedGender = uiState.selectedGender, + selectedSport = uiState.sportSelect, + onGenderSelected = { + pastGamesViewModel.onGenderSelected(it) + }, + onSportSelected = { + pastGamesViewModel.onSportSelected(it) + } + ) + LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { + items(uiState.filteredGames.size) { page -> + val game = uiState.filteredGames[page] + PastGameCard( + teamLogo = game.teamLogo,//painterResource(game.teamLogo), + team = game.team, + date = game.dateString, + genderIcon = painterResource(game.genderIcon), + sportIcon = painterResource(game.sportIcon), + location = game.location, + cornellScore = game.cornellScore!!, + otherScore = game.otherScore!!, + onClick = navigateToGameDetails + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt b/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt index 3f4d54d..a09e045 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt @@ -24,7 +24,12 @@ object Style { fontFamily = poppinsFamily, fontWeight = FontWeight(700), fontStyle = FontStyle.Italic, - color = Color.White + color = Color.White, + shadow = Shadow( + color = Color(0f, 0f, 0f, 0.4f), + offset = Offset(0f, 0f), + blurRadius = 4f + ) ) val universityText = TextStyle( @@ -197,9 +202,40 @@ object Style { fontWeight = FontWeight(400) ) + val metricMedium = TextStyle( + fontSize = 18.sp, + fontFamily = poppinsFamily, + fontWeight = FontWeight(500) + ) + val metricSemibold = TextStyle( fontSize = 18.sp, fontFamily = poppinsFamily, fontWeight = FontWeight(600) ) + + val winningScoreText = TextStyle( + fontSize = 32.sp, + fontFamily = poppinsFamily, + fontWeight = FontWeight.SemiBold, // FontWeight(600) = SemiBold + color = Color.White, + shadow = Shadow( + color = Color(163f, 239f, 32f, 0.5f), // RGBA(163, 239, 32, 0.50) + offset = Offset(0f, 0f), // No offset, just blur + blurRadius = 6f // Matches 6px blur in CSS + ) + ) + + val losingScoreText = TextStyle( + fontSize = 32.sp, + fontFamily = poppinsFamily, + fontWeight = FontWeight.SemiBold, // FontWeight(600) = SemiBold + color = Color.White.copy(alpha = 0.6f), + shadow = Shadow( + color = Color(0f, 0f, 0f, 0.4f), // RGBA(163, 239, 32, 0.50) + offset = Offset(0f, 0f), // No offset, just blur + blurRadius = 4f // Matches 6px blur in CSS + ) + ) + } diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt new file mode 100644 index 0000000..f16147d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt @@ -0,0 +1,163 @@ +package com.cornellappdev.score.viewmodel + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.cornellappdev.score.R +import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.Game +import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.ScoreRepository +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.model.SportSelection +import com.cornellappdev.score.nav.root.RootNavigationRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +data class PastGamesUiState( + val selectedGender: GenderDivision, + val sportSelect: SportSelection, + val selectionList: List, + val pastGameList: List, + // TODO Add remaining dynamic data for UI +) { + val filteredGames: List + get() = pastGameList.filter { game -> + (selectedGender == GenderDivision.ALL || game.gender == selectedGender.displayName) + && (sportSelect is SportSelection.All || (sportSelect is SportSelection.SportSelect && game.sport == sportSelect.sport.displayName)) + } +} + +@HiltViewModel +class PastGamesViewModel @Inject constructor( + private val rootNavigationRepository: RootNavigationRepository, + private val scoreRepository: ScoreRepository +) : BaseViewModel( + initialUiState = PastGamesUiState( + selectedGender = GenderDivision.ALL, + sportSelect = SportSelection.All, + selectionList = Sport.getSportSelectionList(), + pastGameList = emptyList() + ) +) { + fun onGenderSelected(gender: GenderDivision) { + applyMutation { + copy( + selectedGender = gender + ) + } + } + + fun onSportSelected(sport: SportSelection) { + applyMutation { + copy( + sportSelect = sport + ) + } + } + + private fun updateGameList(response: ApiResponse>) { + val games: List = when (response) { + is ApiResponse.Success -> response.data + ApiResponse.Error -> emptyList() + ApiResponse.Loading -> emptyList() + } + + val pastGames = games.filter { game -> + val gameDate = formatDate(game.date) + gameDate != null && gameDate.isBefore(LocalDate.now()) // Only select past games + }.map { game -> + GameCardData( + teamLogo = game.teamLogo, + team = game.teamName, + teamColor = formatColor(game.teamColor), + date = formatDate(game.date), + dateString = dateToString(formatDate(game.date)), + isLive = false, // Past games cannot be live + location = game.city, + gender = game.gender, + genderIcon = if (game.gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, + sport = game.sport, + sportIcon = Sport.fromDisplayName(game.sport)?.emptyIcon + ?: R.drawable.ic_empty_placeholder, + cornellScore = 3, + otherScore = 0 + ) + }.sortedByDescending { it.date } // Sort past games by most recent + + applyMutation { + copy(pastGameList = pastGames) // Rename to pastGameList if needed + } + } + + +// fun onRefresh() { +// viewModelScope.launch { +// val response = scoreRepository.fetchGames() +// updateGameList(response) +// } +// } + + //Converts date from String "month day" to a LocalDate object + private fun formatDate(strDate: String): LocalDate? { + val monthMap = mapOf( + "Jan" to 1, + "Feb" to 2, + "Mar" to 3, + "Apr" to 4, + "May" to 5, + "Jun" to 6, + "Jul" to 7, + "Aug" to 8, + "Sep" to 9, + "Oct" to 10, + "Nov" to 11, + "Dec" to 12 + ) + + val parts = strDate.split(" ") + if (parts.size < 2) return null + + val month = monthMap[parts[0]] + if (month == null) { + return null + } + val day = parts[1].toIntOrNull() ?: return null + + val currentYear = LocalDate.now().year + return LocalDate.of(currentYear, month, day) + } + + /** + * Converts from format "#xxxxxx" to a valid hex, with alpha = 40. Ready to be passed into Color() + */ + + private fun formatColor(color: String): Int { + val alpha = (40 * 255 / 100)// Convert percent to hex (0-255) + val colorInt = Integer.parseInt(color.removePrefix("#"), 16) + return (alpha shl 24) or colorInt + } + + private fun dateToString(date: LocalDate?): String { + if (date == null) { + return "--" + } + return "${date.month.value}/${date.dayOfMonth}/${date.year}" + } + + private fun observePastGames() = scoreRepository.upcomingGamesFlow.onEach { response -> + updateGameList(response) + }.launchIn(viewModelScope) + + init { + observePastGames() + viewModelScope.launch { + scoreRepository.fetchGames() + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..bfb3f18 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseball.xml b/app/src/main/res/drawable/ic_baseball.xml index 7b17ada..09fa23f 100644 --- a/app/src/main/res/drawable/ic_baseball.xml +++ b/app/src/main/res/drawable/ic_baseball.xml @@ -1,9 +1,12 @@ + + android:width="32dp" + android:height="32dp" + android:viewportWidth="32" + android:viewportHeight="32"> + + android:fillColor="#777777" + android:pathData="M7.523,23.07C8.64,22.257 9.505,21.221 10.118,19.963C10.73,18.705 11.036,17.384 11.036,16C11.036,14.616 10.73,13.293 10.118,12.03C9.505,10.767 8.64,9.727 7.523,8.908C6.691,9.909 6.063,11.014 5.641,12.223C5.22,13.433 5.009,14.692 5.009,16C5.009,17.308 5.22,18.565 5.641,19.77C6.063,20.975 6.691,22.075 7.523,23.07ZM16,26.991C17.349,26.991 18.643,26.766 19.883,26.315C21.123,25.863 22.252,25.189 23.272,24.291C22.015,23.267 21.041,22.031 20.349,20.582C19.657,19.134 19.311,17.606 19.311,16C19.311,14.394 19.657,12.864 20.349,11.411C21.041,9.957 22.015,8.719 23.272,7.696C22.252,6.807 21.123,6.137 19.883,5.685C18.643,5.234 17.349,5.009 16,5.009C14.651,5.009 13.357,5.234 12.117,5.685C10.877,6.137 9.748,6.807 8.728,7.696C9.985,8.719 10.962,9.957 11.662,11.411C12.361,12.864 12.711,14.394 12.711,16C12.711,17.606 12.361,19.134 11.662,20.582C10.962,22.031 9.985,23.267 8.728,24.291C9.748,25.189 10.877,25.863 12.117,26.315C13.357,26.766 14.651,26.991 16,26.991ZM24.491,23.07C25.329,22.075 25.955,20.975 26.37,19.77C26.784,18.565 26.991,17.308 26.991,16C26.991,14.692 26.784,13.433 26.37,12.223C25.955,11.014 25.329,9.909 24.491,8.908C23.373,9.727 22.51,10.767 21.9,12.03C21.291,13.293 20.986,14.616 20.986,16C20.986,17.384 21.291,18.705 21.9,19.963C22.51,21.221 23.373,22.257 24.491,23.07ZM16.002,28.667C14.259,28.667 12.617,28.334 11.076,27.669C9.534,27.004 8.191,26.1 7.046,24.955C5.901,23.811 4.996,22.469 4.331,20.928C3.666,19.387 3.333,17.745 3.333,16.002C3.333,14.25 3.666,12.606 4.331,11.069C4.996,9.532 5.901,8.191 7.045,7.046C8.189,5.901 9.531,4.996 11.072,4.331C12.613,3.666 14.255,3.333 15.998,3.333C17.75,3.333 19.394,3.666 20.931,4.331C22.468,4.996 23.809,5.9 24.954,7.045C26.099,8.189 27.004,9.529 27.669,11.065C28.334,12.602 28.667,14.246 28.667,15.998C28.667,17.741 28.334,19.383 27.669,20.924C27.004,22.466 26.1,23.809 24.955,24.954C23.811,26.099 22.471,27.004 20.935,27.669C19.398,28.334 17.754,28.667 16.002,28.667Z"/> + diff --git a/app/src/main/res/drawable/ic_schedule.xml b/app/src/main/res/drawable/ic_schedule.xml new file mode 100644 index 0000000..98cf69f --- /dev/null +++ b/app/src/main/res/drawable/ic_schedule.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_schedule_filled.xml b/app/src/main/res/drawable/ic_schedule_filled.xml new file mode 100644 index 0000000..80edcd6 --- /dev/null +++ b/app/src/main/res/drawable/ic_schedule_filled.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_scores.xml b/app/src/main/res/drawable/ic_scores.xml new file mode 100644 index 0000000..35eab9c --- /dev/null +++ b/app/src/main/res/drawable/ic_scores.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_scores_filled.xml b/app/src/main/res/drawable/ic_scores_filled.xml new file mode 100644 index 0000000..9efff6f --- /dev/null +++ b/app/src/main/res/drawable/ic_scores_filled.xml @@ -0,0 +1,9 @@ + + + From 30c1ca597752ab53dc4175fc5d98fa0cce1dd45a Mon Sep 17 00:00:00 2001 From: helenjb Date: Thu, 20 Mar 2025 00:17:27 -0400 Subject: [PATCH 2/5] Implement networking for final scores of games, minor UI fixes --- app/build.gradle.kts | 1 + app/src/main/graphql/Games.graphql | 5 +++++ .../score/components/GameCard.kt | 2 +- .../score/components/PastGameCard.kt | 10 ++++----- .../com/cornellappdev/score/model/Game.kt | 8 ++++--- .../score/model/ScoreRepository.kt | 22 ++++++++++++++++--- .../score/nav/root/RootNavigation.kt | 4 +++- .../cornellappdev/score/screen/HomeScreen.kt | 4 +++- .../score/screen/PastGamesScreen.kt | 11 +++++++--- .../score/viewmodel/PastGamesViewModel.kt | 6 +++-- gradle/libs.versions.toml | 2 ++ 11 files changed, 56 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2b47b9..af7cf22 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.2") implementation("androidx.compose.material3:material3:1.0.0") implementation("com.google.dagger:hilt-android:2.51.1") + implementation(libs.androidx.media3.common.ktx) kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("com.google.accompanist:accompanist-pager:0.24.0-alpha") diff --git a/app/src/main/graphql/Games.graphql b/app/src/main/graphql/Games.graphql index beb702d..0af8c70 100644 --- a/app/src/main/graphql/Games.graphql +++ b/app/src/main/graphql/Games.graphql @@ -10,5 +10,10 @@ query Games{ color } gender + boxScore { + corScore + oppScore + } + result } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/GameCard.kt b/app/src/main/java/com/cornellappdev/score/components/GameCard.kt index 25ff327..b87e953 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GameCard.kt @@ -94,7 +94,7 @@ fun GameCard( modifier = Modifier.fillMaxWidth() ) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.widthIn(0.dp, 250.dp) ) { AsyncImage( model = teamLogo, diff --git a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt index 6fba7ad..6a8deed 100644 --- a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -57,11 +57,11 @@ fun PastGameCard( genderIcon: Painter, sportIcon: Painter, modifier: Modifier = Modifier, - cornellScore: Int, - otherScore: Int, + cornellScore: Number, + otherScore: Number, onClick: (Boolean) -> Unit = {} ) { - val cornellWins = cornellScore > otherScore + val cornellWins = cornellScore.toFloat() > otherScore.toFloat() val isHome = location == "Ithaca, NY" val firstWins = (cornellWins && isHome) || (!cornellWins && !isHome) @@ -96,7 +96,7 @@ fun PastGameCard( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth()) { - Row(modifier = Modifier.widthIn(0.dp, 170.dp)) { + Row(modifier = Modifier.widthIn(0.dp, 170.dp), verticalAlignment = Alignment.CenterVertically) { if (isHome){ Image( painter = painterResource(R.drawable.cornell_logo), @@ -137,7 +137,7 @@ fun PastGameCard( } Spacer(modifier = Modifier.height(10.dp)) Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.widthIn(0.dp, 170.dp)) { + Row(modifier = Modifier.widthIn(0.dp, 170.dp), verticalAlignment = Alignment.CenterVertically) { if (!isHome){ Image( painter = painterResource(R.drawable.cornell_logo), diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index 1dadb71..6252b90 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -12,7 +12,9 @@ data class Game( val gender: String, val sport: String, val date: String, - val city: String + val city: String, + val cornellScore: Number? = null, + val otherScore: Number? = null, ) //Data for HomeScreen game displays @@ -28,8 +30,8 @@ data class GameCardData( val genderIcon: Int, val sport: String, val sportIcon: Int, - val cornellScore: Int? = null, - val otherScore: Int? = null, + val cornellScore: Number? = null, + val otherScore: Number? = null, ) // Scoring information for a specific team, used in the box score diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 073c0e4..e66e57d 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -37,8 +37,15 @@ class ScoreRepository @Inject constructor( val response = (apolloClient.query(GamesQuery()).execute()) val games = response.data?.games ?: emptyList() Log.d("ScoreRepository", "response fetched successfully") - val list: List = games.mapNotNull { game -> + val scores = game?.result?.split(",")?.getOrNull(1)?.split("-") +// val cornellScore = game?.boxScore?.lastOrNull()?.corScore +// val otherScore = game?.boxScore?.lastOrNull()?.oppScore + val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() + val otherScore = scores?.getOrNull(1)?.toNumberOrNull() + if (game != null) { + Log.d("Scores", " sport: " + game.sport + " date: "+ game.date + " result: DO LATER" + "cornell: " + cornellScore + "other: " + otherScore) + } game?.team?.image?.let { Game( teamLogo = it,//game.team.image, @@ -47,7 +54,9 @@ class ScoreRepository @Inject constructor( gender = game.gender, sport = game.sport, date = game.date, - city = game.city + city = game.city, + cornellScore = cornellScore, + otherScore = otherScore ) } } @@ -58,4 +67,11 @@ class ScoreRepository @Inject constructor( _upcomingGamesFlow.value = ApiResponse.Error } } -} \ No newline at end of file +} + +fun String.toNumberOrNull(): Number? { + return when { + this.contains(".") -> this.toFloatOrNull() // Try converting to Float if there's a decimal + else -> this.toIntOrNull() // Otherwise, try converting to Int + } +} 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 9cd4e93..8c83107 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 @@ -2,6 +2,7 @@ package com.cornellappdev.score.nav.root import android.os.Build import androidx.annotation.RequiresApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -30,6 +31,7 @@ import com.cornellappdev.score.screen.PastGamesScreen import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.Style.bodyMedium +import com.cornellappdev.score.theme.White import kotlinx.serialization.Serializable import java.time.LocalDate @@ -49,7 +51,7 @@ fun RootNavigation( } Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = { - NavigationBar { + NavigationBar(containerColor = White) { tabs.map { item -> val isSelected = item.screen == navBackStackEntry?.toScreen() 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 2b0940f..29b2594 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -2,6 +2,7 @@ package com.cornellappdev.score.screen import android.os.Build import androidx.annotation.RequiresApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -24,6 +25,7 @@ import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.theme.White import com.cornellappdev.score.viewmodel.HomeViewModel @RequiresApi(Build.VERSION_CODES.O)//TODO - change the manifest or leave this? @@ -36,7 +38,7 @@ fun HomeScreen( Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - modifier = Modifier.statusBarsPadding() + modifier = Modifier.statusBarsPadding().background(White) ) { //TODO: check - displaying the earliest three games diff --git a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt index 2604fa0..4ba29fc 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -19,12 +19,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import android.util.Log +import androidx.compose.foundation.background import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.PastGameCard import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.theme.White import com.cornellappdev.score.viewmodel.HomeViewModel import com.cornellappdev.score.viewmodel.PastGamesViewModel @@ -38,7 +41,7 @@ fun PastGamesScreen( Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - modifier = Modifier.statusBarsPadding() + modifier = Modifier.statusBarsPadding().background(White) ) { //TODO: check - displaying the earliest three games @@ -78,10 +81,12 @@ fun PastGamesScreen( genderIcon = painterResource(game.genderIcon), sportIcon = painterResource(game.sportIcon), location = game.location, - cornellScore = game.cornellScore!!, - otherScore = game.otherScore!!, + cornellScore = game.cornellScore ?: -1, + otherScore = game.otherScore ?: -1, onClick = navigateToGameDetails ) + Spacer(modifier = Modifier.height(16.dp)) + Log.d("game", game.toString()) } } } diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt index f16147d..fc8cb06 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt @@ -70,7 +70,9 @@ class PastGamesViewModel @Inject constructor( val pastGames = games.filter { game -> val gameDate = formatDate(game.date) gameDate != null && gameDate.isBefore(LocalDate.now()) // Only select past games + game.cornellScore != null && game.otherScore != null }.map { game -> + Log.d("VM Scores", "sport: " + game.sport + " date: " + game.date + " cornell: " + game.cornellScore + "other: " + game.otherScore) GameCardData( teamLogo = game.teamLogo, team = game.teamName, @@ -84,8 +86,8 @@ class PastGamesViewModel @Inject constructor( sport = game.sport, sportIcon = Sport.fromDisplayName(game.sport)?.emptyIcon ?: R.drawable.ic_empty_placeholder, - cornellScore = 3, - otherScore = 0 + cornellScore = game.cornellScore, + otherScore = game.otherScore ) }.sortedByDescending { it.date } // Sort past games by most recent diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53e3f98..2931c11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ activity = "1.8.0" constraintlayout = "2.1.4" runtimeAndroid = "1.7.2" apollo = "4.1.1" +media3CommonKtx = "1.5.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -23,6 +24,7 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime" } +androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 97828285c0bd994511dc697dd9d710c39c0f13e3 Mon Sep 17 00:00:00 2001 From: helenjb Date: Sat, 22 Mar 2025 15:52:32 -0400 Subject: [PATCH 3/5] Resolve comments --- .../score/components/FeaturedGameCard.kt | 43 +++-- .../score/components/GamesCarousel.kt | 8 +- .../score/components/PastGameCard.kt | 182 ++++++++---------- .../com/cornellappdev/score/model/Game.kt | 17 +- .../score/nav/root/RootNavigation.kt | 55 +++--- .../cornellappdev/score/screen/HomeScreen.kt | 21 +- .../score/screen/PastGamesScreen.kt | 136 ++++++++----- .../score/util/TestingConstants.kt | 2 + .../score/viewmodel/PastGamesViewModel.kt | 151 +++------------ 9 files changed, 294 insertions(+), 321 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt index f076921..3d0d0a1 100644 --- a/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt @@ -39,14 +39,13 @@ import com.cornellappdev.score.theme.Style.winningScoreText fun FeaturedGameHeader( leftTeamLogo: Painter, rightTeamLogo: String, - leftScore: Int? = null, - rightScore: Int? = null, gradientColor1: Color, gradientColor2: Color, - modifier: Modifier = Modifier + isPast: Boolean, + modifier: Modifier = Modifier, + leftScore: Int? = null, + rightScore: Int? = null ) { - val isPast = (leftScore != null && rightScore != null) - Box( modifier = modifier .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) @@ -59,7 +58,10 @@ fun FeaturedGameHeader( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if(isPast) Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally) else Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), + horizontalArrangement = if (isPast) Arrangement.spacedBy( + 12.dp, + Alignment.CenterHorizontally + ) else Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), modifier = Modifier .fillMaxWidth() .padding(vertical = 36.dp) @@ -70,27 +72,28 @@ fun FeaturedGameHeader( contentScale = ContentScale.FillBounds, modifier = Modifier.size(60.dp) ) - if(leftScore != null && rightScore != null) { + if (leftScore != null && rightScore != null) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Text( - text = leftScore.toString(), - style = if(leftScore > rightScore) winningScoreText else losingScoreText, - modifier = Modifier.width(52.dp) + text = leftScore.toString(), + style = if (leftScore > rightScore) winningScoreText else losingScoreText, + modifier = Modifier + .width(52.dp) .wrapContentWidth(Alignment.CenterHorizontally) - ) + ) Text( text = "-", style = vsText.copy(fontSize = 32.sp, fontStyle = FontStyle.Normal) ) Text( text = rightScore.toString(), - style = if(leftScore < rightScore) winningScoreText else losingScoreText, - modifier = Modifier.width(52.dp) - .wrapContentWidth(Alignment.CenterHorizontally) + style = if (leftScore < rightScore) winningScoreText else losingScoreText, + modifier = Modifier + .width(52.dp) + .wrapContentWidth(Alignment.CenterHorizontally) ) } - } - else { + } else { Text( text = "VS", style = vsText @@ -113,6 +116,7 @@ private fun FeaturedGameCardPreview() { rightTeamLogo = "https://cornellbigred.com/images/logos/YALE_LOGO_2020.png?width=80&height=80&mode=max", gradientColor1 = CornellRed, gradientColor2 = PennBlue, + isPast = true, modifier = Modifier ) } @@ -121,11 +125,10 @@ private fun FeaturedGameCardPreview() { fun FeaturedGameCard( leftTeamLogo: Painter, rightTeamLogo: String, - leftScore: Int? = null, - rightScore: Int? = null, team: String, location: String, isLive: Boolean, + isPast: Boolean, genderIcon: Painter, sportIcon: Painter, date: String, @@ -133,6 +136,8 @@ fun FeaturedGameCard( gradientColor2: Color, modifier: Modifier = Modifier, headerModifier: Modifier = Modifier, + leftScore: Int? = null, + rightScore: Int? = null ) { Column( modifier = modifier @@ -146,6 +151,7 @@ fun FeaturedGameCard( rightScore = rightScore, gradientColor1 = gradientColor1, gradientColor2 = gradientColor2, + isPast = isPast, modifier = headerModifier ) @@ -183,6 +189,7 @@ private fun GameScheduleScreen() { location = "Philadelphia, NJ", date = "5/20/2024", isLive = true, + isPast = false, genderIcon = painterResource(id = R.drawable.ic_gender_men), sportIcon = painterResource(id = R.drawable.ic_baseball), modifier = Modifier, diff --git a/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt index 9a81a62..f0a3107 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.model.GamesCarouselVariant import com.cornellappdev.score.theme.CornellRed import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayLight @@ -55,7 +56,7 @@ fun DotIndicator( } @Composable -fun GamesCarousel(games: List, upcoming: Boolean) { +fun GamesCarousel(games: List, variant: GamesCarouselVariant) { val pagerState = rememberPagerState(pageCount = { games.size }) Column( modifier = Modifier @@ -64,7 +65,7 @@ fun GamesCarousel(games: List, upcoming: Boolean) { verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), ) { Text( - text = if (upcoming) "Upcoming" else "Latest", + text = if (variant == GamesCarouselVariant.UPCOMING_VARIANT) "Upcoming" else "Latest", style = heading1, color = GrayPrimary, modifier = Modifier.fillMaxWidth() @@ -81,6 +82,7 @@ fun GamesCarousel(games: List, upcoming: Boolean) { team = game.team, date = game.dateString, isLive = game.isLive, + isPast = game.isPast, genderIcon = painterResource(game.genderIcon), sportIcon = painterResource(game.sportIcon), location = game.location, @@ -104,5 +106,5 @@ fun GamesCarousel(games: List, upcoming: Boolean) { @Preview(showBackground = true, widthDp = 360) @Composable private fun GamesCarouselPreview() { - GamesCarousel(gameList, true) + GamesCarousel(gameList, GamesCarouselVariant.UPCOMING_VARIANT) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt index 6a8deed..420e27c 100644 --- a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.cornellappdev.score.R import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.model.TeamScore import com.cornellappdev.score.theme.AmbientColor import com.cornellappdev.score.theme.GrayLight import com.cornellappdev.score.theme.GrayMedium @@ -56,9 +57,9 @@ fun PastGameCard( location: String, genderIcon: Painter, sportIcon: Painter, - modifier: Modifier = Modifier, cornellScore: Number, otherScore: Number, + modifier: Modifier = Modifier, onClick: (Boolean) -> Unit = {} ) { val cornellWins = cornellScore.toFloat() > otherScore.toFloat() @@ -82,106 +83,28 @@ fun PastGameCard( .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Column(modifier = Modifier - .width(224.dp) - .drawBehind { - drawLine( - color = GrayLight, - start = Offset(size.width, 0f), - end = Offset(size.width, size.height), - strokeWidth = 1.dp.toPx() - ) - }) { - Row(horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth()) { - Row(modifier = Modifier.widthIn(0.dp, 170.dp), verticalAlignment = Alignment.CenterVertically) { - if (isHome){ - Image( - painter = painterResource(R.drawable.cornell_logo), - contentDescription = "Cornell Logo", - modifier = Modifier.height(27.dp).padding(horizontal = 2.dp, vertical = 4.dp) - ) - } else { - AsyncImage( - model = teamLogo, - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), - contentDescription = "" - ) - } - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = if(isHome) "Cornell" else team, - style = heading2, - color = if(firstWins) GrayPrimary else GrayLight + Column( + modifier = Modifier + .width(224.dp) + .drawBehind { + drawLine( + color = GrayLight, + start = Offset(size.width, 0f), + end = Offset(size.width, size.height), + strokeWidth = 1.dp.toPx() ) - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = if(isHome) cornellScore.toString() else otherScore.toString(), - style = metricMedium, - color = if(firstWins) GrayPrimary else GrayLight) - Spacer(modifier = Modifier.width(12.dp)) - if(firstWins) { - Icon(painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = "Indicates winning team", - modifier = Modifier - .width(11.dp) - .height(14.dp),) - } else { - Box(modifier = Modifier.width(11.dp)) - } - } - } + }) { + TeamScore(isHome, teamLogo, team, firstWins, cornellScore, otherScore) Spacer(modifier = Modifier.height(10.dp)) - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.widthIn(0.dp, 170.dp), verticalAlignment = Alignment.CenterVertically) { - if (!isHome){ - Image( - painter = painterResource(R.drawable.cornell_logo), - contentDescription = "Cornell Logo", - modifier = Modifier.height(27.dp).padding(horizontal = 4.dp, vertical = 4.dp) - ) - } else { - AsyncImage( - model = teamLogo, - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), - contentDescription = "" - ) - } - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = if(!isHome) "Cornell" else team, - style = heading2, - color = if(firstWins) GrayLight else GrayPrimary - ) - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = if(!isHome) cornellScore.toString() else otherScore.toString(), - style = metricMedium, - color = if(firstWins) GrayLight else GrayPrimary) - Spacer(modifier = Modifier.width(12.dp)) - if(!firstWins) { - Icon(painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = "Indicates winning team", - modifier = Modifier - .width(11.dp) - .height(14.dp),) - } else { - Box(modifier = Modifier.width(11.dp)) - } - } - } + TeamScore(!isHome, teamLogo, team, !firstWins, cornellScore, otherScore) } Spacer(modifier = Modifier.width(24.dp)) - Column(modifier = Modifier.height(64.dp), - verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.End) { + Column( + modifier = Modifier.height(64.dp), + verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.End + ) { Row( - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -212,6 +135,71 @@ fun PastGameCard( } } +@Composable +private fun TeamScore( + isCornell: Boolean, + teamLogo: String, + team: String, + winningTeam: Boolean, + cornellScore: Number, + otherScore: Number +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + Row( + modifier = Modifier.widthIn(0.dp, 170.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (isCornell) { + Image( + painter = painterResource(R.drawable.cornell_logo), + contentDescription = "Cornell Logo", + modifier = Modifier + .height(27.dp) + .padding(horizontal = 2.dp, vertical = 4.dp) + ) + } else { + AsyncImage( + model = teamLogo, + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + contentDescription = "" + ) + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if (isCornell) "Cornell" else team, + style = heading2, + color = if (winningTeam) GrayPrimary else GrayLight + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = if (cornellScore == -1 && otherScore == -1) "-" else if (isCornell) cornellScore.toString() else otherScore.toString(), + style = metricMedium, + color = if (winningTeam) GrayPrimary else GrayLight + ) + Spacer(modifier = Modifier.width(12.dp)) + if (winningTeam) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = "Indicates winning team", + modifier = Modifier + .width(11.dp) + .height(14.dp), + ) + } else { + Box(modifier = Modifier.width(11.dp)) + } + } + } +} + @Preview(showBackground = true) @Composable private fun PastGameCardPreview() { @@ -220,7 +208,7 @@ private fun PastGameCardPreview() { teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", //painterResource(id = R.drawable.penn_logo), team = "University of Pennsylvania", date = "5/20/2024", - location = "U. Pennsylvania", + location = "Ithaca, NY", genderIcon = painterResource(id = R.drawable.ic_gender_men), sportIcon = painterResource(id = R.drawable.ic_baseball), modifier = Modifier.padding(16.dp), diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index 31d123b..fe0d2d3 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -18,7 +18,13 @@ data class Game( val city: String, val cornellScore: Number? = null, val otherScore: Number? = null, -) +) { + val isPast: Boolean + get() = when { + parseDateOrNull(date)!! < LocalDate.now() -> true + else -> false + } +} //Data for HomeScreen game displays data class GameCardData( @@ -28,6 +34,7 @@ data class GameCardData( val date: LocalDate?, val dateString: String, val isLive: Boolean, + val isPast: Boolean, val location: String, val gender: String, val genderIcon: Int, @@ -107,7 +114,12 @@ enum class GameStatus { COMPLETED } -fun Game.toGameCardData(): GameCardData{ +enum class GamesCarouselVariant { + UPCOMING_VARIANT, + PAST_VARIANT +} + +fun Game.toGameCardData(): GameCardData { return GameCardData( teamLogo = teamLogo, team = teamName, @@ -116,6 +128,7 @@ fun Game.toGameCardData(): GameCardData{ dateString = parseDateOrNull(date)?.format(outputFormatter) ?: date, isLive = (LocalDate.now() == parseDateOrNull(date)), + isPast = isPast, location = city, gender = gender, genderIcon = if (gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, 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 9547113..46181d0 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 @@ -54,15 +54,24 @@ fun RootNavigation( NavigationBarItem( selected = isSelected, onClick = { navController.navigate(item.screen) }, - icon = { Icon(painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon), contentDescription = null, - tint = Color.Unspecified) } , - label = { Text(text = item.label, - style = bodyMedium, - color = if (isSelected) { - CrimsonPrimary - } else { - GrayPrimary - }) } + icon = { + Icon( + painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon), + contentDescription = null, + tint = Color.Unspecified + ) + }, + label = { + Text( + text = item.label, + style = bodyMedium, + color = if (isSelected) { + CrimsonPrimary + } else { + GrayPrimary + } + ) + } ) } } @@ -74,27 +83,21 @@ fun RootNavigation( startDestination = ScoreRootScreens.Home ) { composable { - HomeScreen(navigateToGameDetails = { isPast : Boolean -> - navController.navigate(ScoreRootScreens.GameDetailsPage("", isPast)) + HomeScreen(navigateToGameDetails = { + navController.navigate(ScoreRootScreens.GameDetailsPage("")) }) } - composable { navBackStackEntry -> - val isPast = navBackStackEntry.toRoute().isPast - if(isPast) { - GameDetailsScreen("", onBackArrow = { - navController.navigate(ScoreRootScreens.ScoresScreen) - }) - } else { - GameDetailsScreen("", onBackArrow = { - navController.navigate(ScoreRootScreens.Home) - }) - } + composable { + GameDetailsScreen("", onBackArrow = { + navController.navigateUp() + }) + } composable { - PastGamesScreen(navigateToGameDetails = { isPast : Boolean -> - navController.navigate(ScoreRootScreens.GameDetailsPage("", isPast)) + PastGamesScreen(navigateToGameDetails = { + navController.navigate(ScoreRootScreens.GameDetailsPage("")) }) } } @@ -109,7 +112,7 @@ sealed class ScoreRootScreens { data object Home : ScoreRootScreens() @Serializable - data class GameDetailsPage(val gameId: String, val isPast: Boolean) : ScoreRootScreens() + data class GameDetailsPage(val gameId: String) : ScoreRootScreens() @Serializable data object ScoresScreen : ScoreRootScreens() @@ -119,7 +122,7 @@ sealed class ScoreRootScreens { "Home" -> toRoute() "GameDetailsPage" -> toRoute() "ScoresScreen" -> toRoute() - else -> null + else -> throw IllegalArgumentException("Invalid screen") } } 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 6f875b9..71c0176 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -25,17 +25,14 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.SportSelectorHeader -import com.cornellappdev.score.components.UpcomingGamesCarousel import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.GamesCarouselVariant import com.cornellappdev.score.model.GenderDivision import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.title import com.cornellappdev.score.util.gameList import com.cornellappdev.score.util.sportSelectionList import com.cornellappdev.score.viewmodel.HomeUiState -import com.cornellappdev.score.components.GamesCarousel -import com.cornellappdev.score.theme.Style.heading1 -import com.cornellappdev.score.theme.White import com.cornellappdev.score.viewmodel.HomeViewModel @Composable @@ -47,7 +44,9 @@ fun HomeScreen( Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - modifier = Modifier.statusBarsPadding() + modifier = Modifier + .statusBarsPadding() + .background(Color.White) ) { when (uiState.loadedState) { is ApiResponse.Loading -> { @@ -76,7 +75,8 @@ fun HomeScreen( HomeContent( uiState = uiState, onGenderSelected = { homeViewModel.onGenderSelected(it) }, - onSportSelected = { homeViewModel.onSportSelected(it) } + onSportSelected = { homeViewModel.onSportSelected(it) }, + navigateToGameDetails = navigateToGameDetails ) } } @@ -87,9 +87,10 @@ fun HomeScreen( private fun HomeContent( uiState: HomeUiState, onGenderSelected: (GenderDivision) -> Unit, - onSportSelected: (SportSelection) -> Unit + onSportSelected: (SportSelection) -> Unit, + navigateToGameDetails: (Boolean) -> Unit = {} ) { - GamesCarousel(uiState.upcomingGames) + GamesCarousel(uiState.upcomingGames, GamesCarouselVariant.UPCOMING_VARIANT) Column { Text( text = "Game Schedule", @@ -117,8 +118,10 @@ private fun HomeContent( genderIcon = painterResource(game.genderIcon), sportIcon = painterResource(game.sportIcon), location = game.location, - topCornerRound = true + topCornerRound = true, + onClick = navigateToGameDetails ) + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt index 4ba29fc..d9bbcb9 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -21,74 +21,116 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import android.util.Log import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.ui.graphics.Color import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.PastGameCard +import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.model.GamesCarouselVariant +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.theme.Style.title import com.cornellappdev.score.theme.White +import com.cornellappdev.score.viewmodel.HomeUiState import com.cornellappdev.score.viewmodel.HomeViewModel +import com.cornellappdev.score.viewmodel.PastGamesUiState import com.cornellappdev.score.viewmodel.PastGamesViewModel -@RequiresApi(Build.VERSION_CODES.O)//TODO - change the manifest or leave this? @Composable fun PastGamesScreen( pastGamesViewModel: PastGamesViewModel = hiltViewModel(), - navigateToGameDetails: (Boolean) -> Unit = {} + navigateToGameDetails: (Boolean) -> Unit = {} ) { - val uiState by pastGamesViewModel.uiStateFlow.collectAsState() + val uiState = pastGamesViewModel.collectUiStateValue() Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), - modifier = Modifier.statusBarsPadding().background(White) - ) - { - //TODO: check - displaying the earliest three games - GamesCarousel( - uiState.pastGameList.subList( - 0, - minOf(3, uiState.pastGameList.size) - ), false - ) - Column { - Text( - text = "All Scores", - style = heading1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - SportSelectorHeader( - sports = uiState.selectionList, - selectedGender = uiState.selectedGender, - selectedSport = uiState.sportSelect, - onGenderSelected = { - pastGamesViewModel.onGenderSelected(it) - }, - onSportSelected = { - pastGamesViewModel.onSportSelected(it) + modifier = Modifier + .statusBarsPadding() + .background(Color.White) + ) { + when (uiState.loadedState) { + is ApiResponse.Loading -> { + //TODO: Add loading screen + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - ) - LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { - items(uiState.filteredGames.size) { page -> - val game = uiState.filteredGames[page] - PastGameCard( - teamLogo = game.teamLogo,//painterResource(game.teamLogo), - team = game.team, - date = game.dateString, - genderIcon = painterResource(game.genderIcon), - sportIcon = painterResource(game.sportIcon), - location = game.location, - cornellScore = game.cornellScore ?: -1, - otherScore = game.otherScore ?: -1, - onClick = navigateToGameDetails + } + + is ApiResponse.Error -> { + //TODO: Add Error screen + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Failed to load games. Please try again.", ) - Spacer(modifier = Modifier.height(16.dp)) - Log.d("game", game.toString()) } } + + is ApiResponse.Success -> { + PastGamesContent( + uiState = uiState, + onGenderSelected = { pastGamesViewModel.onGenderSelected(it) }, + onSportSelected = { pastGamesViewModel.onSportSelected(it) }, + navigateToGameDetails = navigateToGameDetails + ) + } + } + } +} + +@Composable +private fun PastGamesContent( + uiState: PastGamesUiState, + onGenderSelected: (GenderDivision) -> Unit, + onSportSelected: (SportSelection) -> Unit, + navigateToGameDetails: (Boolean) -> Unit = {} +) { + GamesCarousel(uiState.pastGames, GamesCarouselVariant.PAST_VARIANT) + Column { + Text( + text = "All Scores", + style = title, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + SportSelectorHeader( + sports = uiState.selectionList, + selectedGender = uiState.selectedGender, + selectedSport = uiState.sportSelect, + onGenderSelected = onGenderSelected, + onSportSelected = onSportSelected + ) + LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { + items(uiState.filteredGames) { + val game = it + PastGameCard( + teamLogo = game.teamLogo, + team = game.team, + date = game.dateString, + genderIcon = painterResource(game.genderIcon), + sportIcon = painterResource(game.sportIcon), + location = game.location, + cornellScore = game.cornellScore ?: -1, + otherScore = game.otherScore ?: -1, + onClick = navigateToGameDetails + ) + Spacer(modifier = Modifier.height(16.dp)) + } } } } \ 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 ac4c45a..12962be 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -18,6 +18,7 @@ val PENN_GAME = GameCardData( date = LocalDate.now(), dateString = "3/1/25", isLive = false, + isPast = true, location = "Philadelphia, PA", gender = "Male", genderIcon = R.drawable.ic_gender_men, @@ -32,6 +33,7 @@ val PRINCETON_GAME = GameCardData( date = LocalDate.now(), dateString = "3/1/25", isLive = false, + isPast = true, location = "Boston, MA", gender = "Female", genderIcon = R.drawable.ic_gender_men, diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt index fc8cb06..440a073 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt @@ -1,20 +1,14 @@ package com.cornellappdev.score.viewmodel -import android.util.Log -import androidx.lifecycle.viewModelScope -import com.cornellappdev.score.R import com.cornellappdev.score.model.ApiResponse -import com.cornellappdev.score.model.Game import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.model.GenderDivision import com.cornellappdev.score.model.ScoreRepository import com.cornellappdev.score.model.Sport import com.cornellappdev.score.model.SportSelection -import com.cornellappdev.score.nav.root.RootNavigationRepository +import com.cornellappdev.score.model.map +import com.cornellappdev.score.model.toGameCardData import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -22,28 +16,50 @@ data class PastGamesUiState( val selectedGender: GenderDivision, val sportSelect: SportSelection, val selectionList: List, - val pastGameList: List, - // TODO Add remaining dynamic data for UI + val loadedState: ApiResponse> ) { + //TODO: refactor filters to use flows - not best practice to expose original games list to the view val filteredGames: List - get() = pastGameList.filter { game -> - (selectedGender == GenderDivision.ALL || game.gender == selectedGender.displayName) - && (sportSelect is SportSelection.All || (sportSelect is SportSelection.SportSelect && game.sport == sportSelect.sport.displayName)) + get() = when (loadedState) { + is ApiResponse.Success -> loadedState.data.filter { game -> + (selectedGender == GenderDivision.ALL || game.gender == selectedGender.displayName) && + (sportSelect is SportSelection.All || (sportSelect is SportSelection.SportSelect && game.sport == sportSelect.sport.displayName)) + } + + ApiResponse.Loading -> emptyList() + ApiResponse.Error -> emptyList() } + val pastGames: List = filteredGames.take(3) } @HiltViewModel class PastGamesViewModel @Inject constructor( - private val rootNavigationRepository: RootNavigationRepository, private val scoreRepository: ScoreRepository ) : BaseViewModel( - initialUiState = PastGamesUiState( + PastGamesUiState( selectedGender = GenderDivision.ALL, sportSelect = SportSelection.All, selectionList = Sport.getSportSelectionList(), - pastGameList = emptyList() + loadedState = ApiResponse.Loading ) ) { + init { + scoreRepository.fetchGames() + asyncCollect(scoreRepository.upcomingGamesFlow) { response -> + applyMutation { + copy( + loadedState = response.map { list -> + list.map { game -> + game.toGameCardData() + }.filter { game -> + game.date?.isBefore(LocalDate.now()) ?: false + }.sortedByDescending { it.date } + } + ) + } + } + } + fun onGenderSelected(gender: GenderDivision) { applyMutation { copy( @@ -59,107 +75,4 @@ class PastGamesViewModel @Inject constructor( ) } } - - private fun updateGameList(response: ApiResponse>) { - val games: List = when (response) { - is ApiResponse.Success -> response.data - ApiResponse.Error -> emptyList() - ApiResponse.Loading -> emptyList() - } - - val pastGames = games.filter { game -> - val gameDate = formatDate(game.date) - gameDate != null && gameDate.isBefore(LocalDate.now()) // Only select past games - game.cornellScore != null && game.otherScore != null - }.map { game -> - Log.d("VM Scores", "sport: " + game.sport + " date: " + game.date + " cornell: " + game.cornellScore + "other: " + game.otherScore) - GameCardData( - teamLogo = game.teamLogo, - team = game.teamName, - teamColor = formatColor(game.teamColor), - date = formatDate(game.date), - dateString = dateToString(formatDate(game.date)), - isLive = false, // Past games cannot be live - location = game.city, - gender = game.gender, - genderIcon = if (game.gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, - sport = game.sport, - sportIcon = Sport.fromDisplayName(game.sport)?.emptyIcon - ?: R.drawable.ic_empty_placeholder, - cornellScore = game.cornellScore, - otherScore = game.otherScore - ) - }.sortedByDescending { it.date } // Sort past games by most recent - - applyMutation { - copy(pastGameList = pastGames) // Rename to pastGameList if needed - } - } - - -// fun onRefresh() { -// viewModelScope.launch { -// val response = scoreRepository.fetchGames() -// updateGameList(response) -// } -// } - - //Converts date from String "month day" to a LocalDate object - private fun formatDate(strDate: String): LocalDate? { - val monthMap = mapOf( - "Jan" to 1, - "Feb" to 2, - "Mar" to 3, - "Apr" to 4, - "May" to 5, - "Jun" to 6, - "Jul" to 7, - "Aug" to 8, - "Sep" to 9, - "Oct" to 10, - "Nov" to 11, - "Dec" to 12 - ) - - val parts = strDate.split(" ") - if (parts.size < 2) return null - - val month = monthMap[parts[0]] - if (month == null) { - return null - } - val day = parts[1].toIntOrNull() ?: return null - - val currentYear = LocalDate.now().year - return LocalDate.of(currentYear, month, day) - } - - /** - * Converts from format "#xxxxxx" to a valid hex, with alpha = 40. Ready to be passed into Color() - */ - - private fun formatColor(color: String): Int { - val alpha = (40 * 255 / 100)// Convert percent to hex (0-255) - val colorInt = Integer.parseInt(color.removePrefix("#"), 16) - return (alpha shl 24) or colorInt - } - - private fun dateToString(date: LocalDate?): String { - if (date == null) { - return "--" - } - return "${date.month.value}/${date.dayOfMonth}/${date.year}" - } - - private fun observePastGames() = scoreRepository.upcomingGamesFlow.onEach { response -> - updateGameList(response) - }.launchIn(viewModelScope) - - init { - observePastGames() - viewModelScope.launch { - scoreRepository.fetchGames() - } - } - } \ No newline at end of file From 0dfa7d00e0d299e61bd958a3303572c57a6726fb Mon Sep 17 00:00:00 2001 From: helenjb Date: Sat, 22 Mar 2025 17:26:09 -0400 Subject: [PATCH 4/5] Fix gender filter --- .../java/com/cornellappdev/score/model/ScoreRepository.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 2894b80..013353b 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -48,8 +48,8 @@ class ScoreRepository @Inject constructor( Game( teamLogo = it, teamName = game.team.name, - teamColor = parseColor(game.team.color).copy(alpha = 0.4f*255), - gender = game.gender, + teamColor = parseColor(game.team.color).copy(alpha = 0.4f * 255), + gender = if (game.gender == "Mens") "Men's" else "Women's", sport = game.sport, date = game.date, city = game.city, From e677db49a7ecbd3658ec0e3082689dce71b16be6 Mon Sep 17 00:00:00 2001 From: helenjb Date: Mon, 24 Mar 2025 15:51:50 -0400 Subject: [PATCH 5/5] Resolve comments --- .../score/components/GamesCarousel.kt | 4 +- .../score/components/PastGameCard.kt | 81 +++++++++---------- .../com/cornellappdev/score/model/Game.kt | 20 +++-- .../score/model/ScoreRepository.kt | 9 +++ .../cornellappdev/score/screen/HomeScreen.kt | 2 +- .../score/screen/PastGamesScreen.kt | 11 +-- .../score/viewmodel/PastGamesViewModel.kt | 1 - 7 files changed, 67 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt index f0a3107..2f1e652 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt @@ -65,7 +65,7 @@ fun GamesCarousel(games: List, variant: GamesCarouselVariant) { verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), ) { Text( - text = if (variant == GamesCarouselVariant.UPCOMING_VARIANT) "Upcoming" else "Latest", + text = if (variant == GamesCarouselVariant.UPCOMING) "Upcoming" else "Latest", style = heading1, color = GrayPrimary, modifier = Modifier.fillMaxWidth() @@ -106,5 +106,5 @@ fun GamesCarousel(games: List, variant: GamesCarouselVariant) { @Preview(showBackground = true, widthDp = 360) @Composable private fun GamesCarouselPreview() { - GamesCarousel(gameList, GamesCarouselVariant.UPCOMING_VARIANT) + GamesCarousel(gameList, GamesCarouselVariant.UPCOMING) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt index 420e27c..a3c5c66 100644 --- a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -1,23 +1,13 @@ package com.cornellappdev.score.components -import android.graphics.drawable.Icon -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -27,45 +17,29 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.cornellappdev.score.R import com.cornellappdev.score.model.GameCardData -import com.cornellappdev.score.model.TeamScore import com.cornellappdev.score.theme.AmbientColor import com.cornellappdev.score.theme.GrayLight import com.cornellappdev.score.theme.GrayMedium import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.SpotColor -import com.cornellappdev.score.theme.Style.bodyNormal import com.cornellappdev.score.theme.Style.heading2 import com.cornellappdev.score.theme.Style.labelsNormal import com.cornellappdev.score.theme.Style.metricMedium -import com.cornellappdev.score.theme.saturatedGreen +import java.time.LocalDate @Composable fun PastGameCard( - teamLogo: String, - team: String, - date: String, - location: String, - genderIcon: Painter, - sportIcon: Painter, - cornellScore: Number, - otherScore: Number, + data: GameCardData, modifier: Modifier = Modifier, onClick: (Boolean) -> Unit = {} ) { - val cornellWins = cornellScore.toFloat() > otherScore.toFloat() - val isHome = location == "Ithaca, NY" - val firstWins = (cornellWins && isHome) || (!cornellWins && !isHome) - Card( colors = CardDefaults.cardColors(containerColor = Color.White), modifier = modifier @@ -94,9 +68,23 @@ fun PastGameCard( strokeWidth = 1.dp.toPx() ) }) { - TeamScore(isHome, teamLogo, team, firstWins, cornellScore, otherScore) + TeamScore( + data.isHome, + data.teamLogo, + data.team, + data.firstTeamListedWins, + data.cornellScore ?: -1, + data.otherScore ?: -1 + ) Spacer(modifier = Modifier.height(10.dp)) - TeamScore(!isHome, teamLogo, team, !firstWins, cornellScore, otherScore) + TeamScore( + !data.isHome, + data.teamLogo, + data.team, + !data.firstTeamListedWins, + data.cornellScore ?: -1, + data.otherScore ?: -1 + ) } Spacer(modifier = Modifier.width(24.dp)) Column( @@ -108,7 +96,7 @@ fun PastGameCard( verticalAlignment = Alignment.CenterVertically ) { Icon( - painter = sportIcon, + painter = painterResource(data.sportIcon), contentDescription = "Sport Icon", modifier = Modifier .width(24.dp) @@ -116,7 +104,7 @@ fun PastGameCard( tint = Color.Unspecified ) Icon( - painter = genderIcon, + painter = painterResource(data.genderIcon), contentDescription = "Gender Icon", modifier = Modifier .padding(2.5.dp) @@ -126,7 +114,7 @@ fun PastGameCard( ) } Text( - text = date, + text = data.dateString, style = labelsNormal, color = GrayMedium ) @@ -203,17 +191,26 @@ private fun TeamScore( @Preview(showBackground = true) @Composable private fun PastGameCardPreview() { + val gameCard = GameCardData( + teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", + team = "University of Pennsylvania", + teamColor = Color.Red, + date = LocalDate.of(2025, 3, 24), + dateString = "March 24, 2025", + isLive = false, + isPast = true, + location = "Ithaca, NY", + gender = "Men", + genderIcon = R.drawable.ic_gender_men, // replace with your actual drawable resource + sport = "Basketball", + sportIcon = R.drawable.ic_basketball, // replace with your actual drawable resource + cornellScore = 85, + otherScore = 80 + ) + Column { PastGameCard( - teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", //painterResource(id = R.drawable.penn_logo), - team = "University of Pennsylvania", - date = "5/20/2024", - location = "Ithaca, NY", - genderIcon = painterResource(id = R.drawable.ic_gender_men), - sportIcon = painterResource(id = R.drawable.ic_baseball), - modifier = Modifier.padding(16.dp), - cornellScore = 3, - otherScore = 0 + data = gameCard ) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index fe0d2d3..b2e6c79 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -20,9 +20,9 @@ data class Game( val otherScore: Number? = null, ) { val isPast: Boolean - get() = when { - parseDateOrNull(date)!! < LocalDate.now() -> true - else -> false + get() { + val parsedDate = parseDateOrNull(date) ?: LocalDate.MAX + return parsedDate < LocalDate.now() } } @@ -40,9 +40,17 @@ data class GameCardData( val genderIcon: Int, val sport: String, val sportIcon: Int, + val isHome: Boolean = location == "Ithaca, NY", val cornellScore: Number? = null, val otherScore: Number? = null, -) +) { + val firstTeamListedWins: Boolean + get() { + val cornellWins = (cornellScore?.toFloat() ?: 0f) > (otherScore?.toFloat() ?: 0f) + val firstWins = (cornellWins && isHome) || (!cornellWins && !isHome) + return firstWins + } +} // Scoring information for a specific team, used in the box score data class TeamScore( @@ -115,8 +123,8 @@ enum class GameStatus { } enum class GamesCarouselVariant { - UPCOMING_VARIANT, - PAST_VARIANT + UPCOMING, + PAST } fun Game.toGameCardData(): GameCardData { diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 013353b..edac55b 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -41,6 +41,15 @@ class ScoreRepository @Inject constructor( val gamesList: List = games?.games?.mapNotNull { game -> + /** + * The final scores in the past game cards are obtained by parsing a String + * result from the GameQuery, which is oftentimes in the format + * Result, CornellScore-OpponentScore (e.g. "W, 2-1"). Not all of the strings + * are in this format (e.g. 4th of 6, 1498 points for women's Swimming and + * Diving), but in this case, the cornellScore and otherScore parameters of + * the game and associated card should be null, and as of right now, + * null-scored games are filtered out. + */ val scores = game?.result?.split(",")?.getOrNull(1)?.split("-") val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() val otherScore = scores?.getOrNull(1)?.toNumberOrNull() 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 71c0176..5e5520a 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -90,7 +90,7 @@ private fun HomeContent( onSportSelected: (SportSelection) -> Unit, navigateToGameDetails: (Boolean) -> Unit = {} ) { - GamesCarousel(uiState.upcomingGames, GamesCarouselVariant.UPCOMING_VARIANT) + GamesCarousel(uiState.upcomingGames, GamesCarouselVariant.UPCOMING) Column { Text( text = "Game Schedule", diff --git a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt index d9bbcb9..5c1784e 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -98,7 +98,7 @@ private fun PastGamesContent( onSportSelected: (SportSelection) -> Unit, navigateToGameDetails: (Boolean) -> Unit = {} ) { - GamesCarousel(uiState.pastGames, GamesCarouselVariant.PAST_VARIANT) + GamesCarousel(uiState.pastGames, GamesCarouselVariant.PAST) Column { Text( text = "All Scores", @@ -119,14 +119,7 @@ private fun PastGamesContent( items(uiState.filteredGames) { val game = it PastGameCard( - teamLogo = game.teamLogo, - team = game.team, - date = game.dateString, - genderIcon = painterResource(game.genderIcon), - sportIcon = painterResource(game.sportIcon), - location = game.location, - cornellScore = game.cornellScore ?: -1, - otherScore = game.otherScore ?: -1, + data = game, onClick = navigateToGameDetails ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt index 440a073..4df8393 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt @@ -44,7 +44,6 @@ class PastGamesViewModel @Inject constructor( ) ) { init { - scoreRepository.fetchGames() asyncCollect(scoreRepository.upcomingGamesFlow) { response -> applyMutation { copy(