diff --git a/app/src/main/graphql/GameById.graphql b/app/src/main/graphql/GameById.graphql new file mode 100644 index 0000000..aced4b7 --- /dev/null +++ b/app/src/main/graphql/GameById.graphql @@ -0,0 +1,32 @@ +query GameById($id: String!) { + game(id: $id){ + id + city + date + gender + location + opponentId + result + sport + state + time + scoreBreakdown + team { + id + color + image + name + } + boxScore { + team + period + time + description + scorer + assist + scoreBy + corScore + oppScore + } + } +} \ No newline at end of file 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 ed55c55..bcb5bc4 100644 --- a/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/FeaturedGameCard.kt @@ -2,6 +2,7 @@ package com.cornellappdev.score.components import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -137,11 +138,13 @@ fun FeaturedGameCard( modifier: Modifier = Modifier, headerModifier: Modifier = Modifier, leftScore: Int? = null, - rightScore: Int? = null + rightScore: Int? = null, + onClick: () -> Unit = {} ) { Column( modifier = modifier .fillMaxWidth() + .clickable { onClick() } ) { FeaturedGameHeader( @@ -172,7 +175,8 @@ fun FeaturedGameCard( bottomStart = 16.dp, bottomEnd = 16.dp ) - ) + ), + onClick = onClick ) } } 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 7d28d31..7c7da7b 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GameCard.kt @@ -57,7 +57,7 @@ fun GameCard( sportIcon: Painter, topCornerRound: Boolean, modifier: Modifier = Modifier, - onClick: (Boolean) -> Unit = {} + onClick: () -> Unit ) { val cardShape = if (topCornerRound) { RoundedCornerShape(16.dp) // Rounded all @@ -89,7 +89,7 @@ fun GameCard( ) } ) - .clickable { onClick(false) } + .clickable { onClick() } ) { Column( modifier = Modifier @@ -219,6 +219,7 @@ private fun GameCardPreview() = ScorePreview { sportIcon = painterResource(id = R.drawable.ic_baseball), topCornerRound = false, modifier = Modifier.padding(16.dp), + onClick = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/GameScoreHeader.kt b/app/src/main/java/com/cornellappdev/score/components/GameScoreHeader.kt index 898c5e7..bacee06 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GameScoreHeader.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GameScoreHeader.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign 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.theme.Style.scoreHeaderText import com.cornellappdev.score.theme.Style.vsText @@ -35,9 +36,11 @@ import com.cornellappdev.score.theme.Style.vsText @Composable fun GameScoreHeader( leftTeamLogo: Painter, - rightTeamLogo: Painter, + rightTeamLogo: String, gradientColor1: Color, gradientColor2: Color, + leftScore: Int, + rightScore: Int, modifier: Modifier = Modifier ) { Box( @@ -64,7 +67,7 @@ fun GameScoreHeader( Row { Text( - text = "0", + text = leftScore.toString(), style = scoreHeaderText, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center @@ -76,15 +79,15 @@ fun GameScoreHeader( ) Text( - text = "0", + text = rightScore.toString(), style = scoreHeaderText, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center ) } - Image( - painter = rightTeamLogo, + AsyncImage( + model = rightTeamLogo, contentDescription = "Right Team Logo", modifier = Modifier.height(70.dp) ) @@ -97,9 +100,11 @@ fun GameScoreHeader( private fun GameScoreHeaderPreview() = ScorePreview { GameScoreHeader( leftTeamLogo = painterResource(R.drawable.cornell_logo), - rightTeamLogo = painterResource(R.drawable.penn_logo), + rightTeamLogo = "https://images.sidearmdev.com/fit?url=https%3a%2f%2fdxbhsrqyrr690.cloudfront.net%2fsidearm.nextgen.sites%2fcornellbigred.com%2fimages%2flogos%2fpenn_200x200.png&height=80&width=80&type=webp", gradientColor1 = Color(0xFFE1A69F), gradientColor2 = Color(0xFF011F5B), + leftScore = 0, + rightScore = 0, modifier = Modifier.height(185.dp) ) } \ No newline at end of file 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 84f957d..b8f8016 100644 --- a/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt +++ b/app/src/main/java/com/cornellappdev/score/components/GamesCarousel.kt @@ -55,7 +55,8 @@ fun DotIndicator( @Composable fun GamesCarousel( games: List, - modifier: Modifier = Modifier + onClick: (String) -> Unit, + modifier: Modifier = Modifier, ) { val pagerState = rememberPagerState(pageCount = { games.size }) Column( @@ -83,7 +84,8 @@ fun GamesCarousel( modifier = Modifier, headerModifier = Modifier, gradientColor1 = CornellRed, - gradientColor2 = game.teamColor + gradientColor2 = game.teamColor, + onClick = { onClick(game.id) } ) } @@ -100,5 +102,5 @@ fun GamesCarousel( @Composable @Preview private fun GamesCarouselPreview() = ScorePreview { - GamesCarousel(gameList) + GamesCarousel(gameList, onClick = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/NavigationHeader.kt b/app/src/main/java/com/cornellappdev/score/components/NavigationHeader.kt index 1022b17..452679a 100644 --- a/app/src/main/java/com/cornellappdev/score/components/NavigationHeader.kt +++ b/app/src/main/java/com/cornellappdev/score/components/NavigationHeader.kt @@ -12,7 +12,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -24,12 +23,12 @@ import com.cornellappdev.score.theme.Style.heading2 fun NavigationHeader(title: String, onBackPressed: () -> Unit) { Box( modifier = Modifier - .shadow(elevation = 8.dp, clip = false, spotColor = Color.Black.copy(0.05f)) + //.shadow(elevation = 8.dp, clip = false, spotColor = Color.Black.copy(0.05f)) .background(Color.White) ) { Box( modifier = Modifier - .padding(start = 24.dp, top = 56.dp, bottom = 12.dp, end = 24.dp) + .padding(start = 24.dp, top = 24.dp, bottom = 12.dp, end = 24.dp) .background(Color.White) .fillMaxWidth() .height(27.dp) @@ -55,6 +54,6 @@ fun NavigationHeader(title: String, onBackPressed: () -> Unit) { @Preview @Composable -private fun NavigationHeaderPreview() = ScorePreview { - NavigationHeader("Game Details", {}) +private fun NavigationHeaderPreview() { + NavigationHeader("Game Details", {}) } \ 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 fcc8506..6e09ae5 100644 --- a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -47,7 +47,7 @@ import java.time.LocalDate fun PastGameCard( data: GameCardData, modifier: Modifier = Modifier, - onClick: (Boolean) -> Unit = {} + onClick: () -> Unit = {} ) { Card( colors = CardDefaults.cardColors(containerColor = Color.White), @@ -58,7 +58,7 @@ fun PastGameCard( Modifier .border(width = 1.dp, color = GrayStroke, RoundedCornerShape(16.dp)) ) - .clickable { onClick(true) } + .clickable { onClick() } ) { Row( modifier = Modifier @@ -201,6 +201,7 @@ private fun TeamScore( @Composable private fun PastGameCardPreview() = ScorePreview { val gameCard = GameCardData( + id = "1", teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", team = "University of Pennsylvania", teamColor = Color.Red, diff --git a/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt b/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt index 3cab34a..b85e757 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.model.GameData @@ -102,7 +103,9 @@ fun TeamScoreRow(teamScore: TeamScore, totalTextColor: Color) { style = bodyNormal, color = GrayPrimary, modifier = Modifier.weight(1f), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) teamScore.scoresByPeriod.forEach { score -> diff --git a/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt b/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt index 6c4218e..cca3322 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign 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.components.ScorePreview import com.cornellappdev.score.model.ScoreEvent import com.cornellappdev.score.theme.GrayPrimary @@ -46,13 +48,25 @@ fun ScoreEventItem(event: ScoreEvent) { .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - Image( - painter = painterResource(event.team.logo), - contentDescription = event.team.name, - modifier = Modifier - .size(40.dp) - .padding(end = 12.dp) - ) + if (event.team.name == "COR"){ // TODO: Check if its "COR" for all queries. It is for baseball + Image( + painter = painterResource(R.drawable.cornell_logo), + contentDescription = event.team.name, + modifier = Modifier + .size(40.dp) + .padding(end = 12.dp) + ) + } + else{ + AsyncImage( + model = event.team.logo, + contentDescription = event.team.name, // Turn this into a if statement if i know the link for cornell logo + modifier = Modifier + .size(40.dp) + .padding(end = 12.dp) + ) + } + Row( modifier = Modifier.weight(2f), @@ -90,7 +104,7 @@ fun ScoreEventItem(event: ScoreEvent) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = homeScore.toString(), - style = if (event.team.name == "Cornell") metricSemibold else metricNormal, + style = if (event.team.name == "Cornell") metricSemibold else metricNormal, // TODO: Check name color = GrayPrimary, textAlign = TextAlign.Center ) 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 b2e6c79..d6dfd42 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -2,13 +2,21 @@ package com.cornellappdev.score.model import androidx.compose.ui.graphics.Color import com.cornellappdev.score.R +import com.cornellappdev.score.util.convertScores +import com.cornellappdev.score.util.formatDateTimeDisplay +import com.cornellappdev.score.util.getTimeUntilStart import com.cornellappdev.score.util.outputFormatter import com.cornellappdev.score.util.parseDateOrNull +import com.cornellappdev.score.util.parseDateTimeOrNull +import com.cornellappdev.score.util.parseResultScore +import com.cornellappdev.score.util.toGameData import java.time.LocalDate +import java.time.LocalDateTime // TODO Refactor to make easier to filter... actual gender, etc. data class Game( + val id: String, val teamName: String, val teamLogo: String, val teamColor: Color, @@ -26,8 +34,45 @@ data class Game( } } -//Data for HomeScreen game displays +data class GameDetailsTeam( + val id: String?, + val color: Color, + val image: String?, + val name: String +) + +data class GameDetailsBoxScore( + val team: String?, + val period: String?, + val time: String?, + val description: String?, + val scorer: String?, + val assist: String?, + val scoreBy: String?, + val corScore: Int?, + val oppScore: Int? +) + +data class GameDetailsGame( + val id: String?, + val city: String, + val date: String, + val gender: String, + val location: String?, + val opponentId: String, + val result: String?, + val sport: String, + val state: String, + val time: String?, + val scoreBreakdown: List?>?, + val team: GameDetailsTeam?, + val boxScore: List? +) + + +// Data for HomeScreen game displays data class GameCardData( + val id: String, val teamLogo: String, val team: String, val teamColor: Color, @@ -52,9 +97,35 @@ data class GameCardData( } } +// Data for GameDetailsScreen +data class DetailsCardData( + val title: String, + val opponentLogo: String, + val opponent: String, + val opponentColor: Color, + val date: LocalDate?, + val time: String, + val dateString: String, + val isPastStartTime: Boolean, + val location: String, + val locationString: String, + val gender: String, + val genderIcon: Int, + val sport: String, + val sportIcon: Int, + val boxScore: List, + val scoreBreakdown: List?>?, + val gameData: GameData, + val scoreEvent: List, + val daysUntilGame: Int?, + val hoursUntilGame: Int?, + val homeScore: Int, + val oppScore: Int +) + // Scoring information for a specific team, used in the box score data class TeamScore( - val team: Team, + val team: TeamBoxScore, val scoresByPeriod: List, val totalScore: Int ) @@ -79,7 +150,7 @@ data class ScoreEvent( val id: Int, val time: String, val quarter: String, - val team: Team, + val team: TeamGameSummary, val eventType: String, val score: String, val description: String? = null @@ -89,9 +160,13 @@ data class ScoreEvent( val awayScore get() = scoreTuple[1] } -data class Team( +data class TeamBoxScore( + val name: String +) + +data class TeamGameSummary( val name: String, - val logo: Int + val logo: String ) data class GameSummary( @@ -129,6 +204,7 @@ enum class GamesCarouselVariant { fun Game.toGameCardData(): GameCardData { return GameCardData( + id = id, teamLogo = teamLogo, team = teamName, teamColor = teamColor, @@ -144,4 +220,61 @@ fun Game.toGameCardData(): GameCardData { sportIcon = Sport.fromDisplayName(sport)?.emptyIcon ?: R.drawable.ic_empty_placeholder ) +} + +fun GameDetailsGame.toGameCardData(): DetailsCardData { + val (daysUntil, hoursUntil) = getTimeUntilStart(date, time ?: "") ?: (null to null) + val parsedScores = parseResultScore(result) + return DetailsCardData( + title = "Cornell Vs. ${team?.name ?: ""}", + opponentLogo = team?.image ?: "", + opponent = team?.name ?: "", + opponentColor = team?.color ?: Color.White, + date = parseDateOrNull(date), + time = time ?: "", + dateString = formatDateTimeDisplay(date, time ?: ""), + isPastStartTime = parseDateTimeOrNull(date, time ?: "")?.let { + !LocalDateTime.now().isBefore(it) + } ?: false, + location = city, + locationString = "${city}, ${state}", + gender = gender, + genderIcon = if (gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, + sport = sport, + sportIcon = Sport.fromDisplayName(sport)?.emptyIcon + ?: R.drawable.ic_empty_placeholder, + boxScore = boxScore ?: emptyList(), + scoreBreakdown = scoreBreakdown ?: emptyList(), + gameData = toGameData( + scoreBreakdown = scoreBreakdown, + team1 = TeamBoxScore("Cornell"), + team2 = TeamBoxScore(team?.name ?: ""), + sport = sport + ), + scoreEvent = boxScore?.toScoreEvents(team?.image ?: "") ?: emptyList(), + daysUntilGame = daysUntil, + hoursUntilGame = hoursUntil, + homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport).second + ?: parsedScores?.first ?: 0, + oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport).second + ?: parsedScores?.second ?: 0 + ) +} + +fun List.toScoreEvents(teamLogo: String): List { + return this.mapIndexed { index, boxScore -> + val teamName = boxScore.team ?: "" + val corScore = boxScore.corScore ?: 0 + val oppScore = boxScore.oppScore ?: 0 + + ScoreEvent( + id = index, + time = boxScore.time ?: "", + quarter = boxScore.period ?: "", + team = TeamGameSummary(teamName, logo = teamLogo), + eventType = "Score", // TODO: Change to what ios has and not figma + score = "$corScore - $oppScore", + description = boxScore.description + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/GameByIdQueryMappers.kt b/app/src/main/java/com/cornellappdev/score/model/GameByIdQueryMappers.kt new file mode 100644 index 0000000..81ad01a --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/model/GameByIdQueryMappers.kt @@ -0,0 +1,45 @@ +package com.cornellappdev.score.model + +import com.cornellappdev.score.util.parseColor +import com.example.score.GameByIdQuery + +fun GameByIdQuery.Game.toGameDetails(): GameDetailsGame { + return GameDetailsGame( + id = this.id ?: "", + city = this.city, + date = this.date, + gender = this.gender, + location = this.location, + opponentId = this.opponentId, + result = this.result, + sport = this.sport, + state = this.state, + time = this.time, + scoreBreakdown = this.scoreBreakdown, + team = this.team?.toGameDetailsTeam(), + boxScore = this.boxScore?.mapNotNull { it?.toGameDetailsBoxScore() } + ) +} +fun GameByIdQuery.Team.toGameDetailsTeam(): GameDetailsTeam { + return GameDetailsTeam( + id = this.id, + color = parseColor(this.color).copy(alpha = 0.4f*255), + image = this.image, + name = this.name + ) +} + +fun GameByIdQuery.BoxScore.toGameDetailsBoxScore(): GameDetailsBoxScore { + return GameDetailsBoxScore( + team = this.team, + period = this.period, + time = this.time, + description = this.description, + scorer = this.scorer, + assist = this.assist, + scoreBy = this.scoreBy, + corScore = this.corScore, + oppScore = this.oppScore + ) +} + 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 edac55b..43cce31 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -2,14 +2,16 @@ package com.cornellappdev.score.model import android.util.Log import com.apollographql.apollo.ApolloClient +import com.cornellappdev.score.util.parseColor +import com.example.score.GameByIdQuery import com.example.score.GamesQuery import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton -import com.cornellappdev.score.util.parseColor /** * This is a singleton responsible for fetching and caching all data for Score. @@ -26,6 +28,9 @@ class ScoreRepository @Inject constructor( MutableStateFlow>>(ApiResponse.Loading) val upcomingGamesFlow = _upcomingGamesFlow.asStateFlow() + private val _currentGameFlow = + MutableStateFlow>(ApiResponse.Loading) + val currentGamesFlow = _currentGameFlow.asStateFlow() /** * Asynchronously fetches the list of games from the API. Once finished, will send down @@ -55,6 +60,7 @@ class ScoreRepository @Inject constructor( val otherScore = scores?.getOrNull(1)?.toNumberOrNull() game?.team?.image?.let { Game( + id = game.id ?: "", // Should never be null teamLogo = it, teamName = game.team.name, teamColor = parseColor(game.team.color).copy(alpha = 0.4f * 255), @@ -77,6 +83,24 @@ class ScoreRepository @Inject constructor( _upcomingGamesFlow.value = ApiResponse.Error } } + + /** + * Asynchronously fetches game details for a particular game. Once finished, will update + * `currentGamesFlow` to be observed. + */ + fun getGameById(id: String) = appScope.launch { + _currentGameFlow.value = ApiResponse.Loading + try { + val result = (apolloClient.query(GameByIdQuery(id)).execute()).toResult() + result.getOrNull()?.game?.let { + _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) + } ?: _currentGameFlow.update { ApiResponse.Error } + } catch (e: Exception) { + Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) + _currentGameFlow.value = ApiResponse.Error + } + } + } fun String.toNumberOrNull(): Number? { 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 696eec8..a3c8abc 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 @@ -68,6 +68,9 @@ fun RootNavigation( } Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = { + if (navBackStackEntry?.toScreen() is ScoreRootScreens.GameDetailsPage) { + return@Scaffold + } NavigationBar(containerColor = White) { tabs.map { item -> val isSelected = item.screen == navBackStackEntry?.toScreen() @@ -106,12 +109,12 @@ fun RootNavigation( ) { composable { HomeScreen(navigateToGameDetails = { - navController.navigate(ScoreRootScreens.GameDetailsPage("")) + navController.navigate(ScoreRootScreens.GameDetailsPage(it)) }) } composable { - GameDetailsScreen("", onBackArrow = { + GameDetailsScreen(onBackArrow = { navController.navigateUp() }) @@ -119,7 +122,7 @@ fun RootNavigation( composable { PastGamesScreen(navigateToGameDetails = { - navController.navigate(ScoreRootScreens.GameDetailsPage("")) + navController.navigate(ScoreRootScreens.GameDetailsPage(it)) }) } } 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 4444fdc..554185d 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -1,38 +1,103 @@ package com.cornellappdev.score.screen +import ScoringSummary import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.R +import com.cornellappdev.score.components.BoxScore import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.GameScoreHeader import com.cornellappdev.score.components.NavigationHeader -import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.components.TimeUntilStartCard +import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.DetailsCardData +import com.cornellappdev.score.model.GameData +import com.cornellappdev.score.model.GameDetailsBoxScore +import com.cornellappdev.score.model.ScoreEvent +import com.cornellappdev.score.model.TeamBoxScore +import com.cornellappdev.score.model.TeamGameSummary +import com.cornellappdev.score.model.TeamScore import com.cornellappdev.score.theme.GrayMedium import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.Style.bodyNormal import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.theme.Style.heading2 import com.cornellappdev.score.theme.Style.heading3 import com.cornellappdev.score.theme.White +import com.cornellappdev.score.util.addToCalendar +import com.cornellappdev.score.util.toCalendarEvent +import com.cornellappdev.score.viewmodel.GameDetailsViewModel +import java.time.LocalDate @Composable -fun GameDetailsScreen(gameId: String = "", onBackArrow: () -> Unit = {}) { +fun GameDetailsScreen( + gameDetailsViewModel: GameDetailsViewModel = hiltViewModel(), + onBackArrow: () -> Unit = {} +) { + val uiState = gameDetailsViewModel.collectUiStateValue() + Column( + modifier = Modifier + .fillMaxSize() + .background(White) + ) { + NavigationHeader( + title = "Game Details", + onBackPressed = onBackArrow + ) + when (val state = uiState.loadedState) { + is ApiResponse.Loading, ApiResponse.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = GrayPrimary) + } + } + + is ApiResponse.Error -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Failed to load game.") + } + } + + is ApiResponse.Success -> { + GameDetailsContent( + gameCard = state.data + ) + } + } + } +} + +@Composable +fun GameDetailsContent(gameCard: DetailsCardData) { Column( modifier = Modifier .background(White) @@ -40,12 +105,13 @@ fun GameDetailsScreen(gameId: String = "", onBackArrow: () -> Unit = {}) { horizontalAlignment = Alignment.CenterHorizontally ) { // TODO: add navigation - NavigationHeader(title = "Game Details", onBackArrow) GameScoreHeader( leftTeamLogo = painterResource(R.drawable.cornell_logo), - rightTeamLogo = painterResource(R.drawable.penn_logo), + rightTeamLogo = gameCard.opponentLogo, gradientColor1 = Color(0xFFE1A69F), - gradientColor2 = Color(0xFF011F5B), + gradientColor2 = gameCard.opponentColor, + leftScore = gameCard.homeScore, + rightScore = gameCard.oppScore, modifier = Modifier.height(185.dp) ) @@ -53,14 +119,14 @@ fun GameDetailsScreen(gameId: String = "", onBackArrow: () -> Unit = {}) { Column(Modifier.padding(horizontal = 24.dp)) { Text( - text = "Men's Football", + text = gameCard.sport, style = heading3.copy(color = GrayPrimary) ) Text( - text = "Cornell vs. Yale", + text = gameCard.title, style = heading1.copy(color = GrayPrimary) ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(13.5.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Image( @@ -72,123 +138,165 @@ fun GameDetailsScreen(gameId: String = "", onBackArrow: () -> Unit = {}) { colorFilter = ColorFilter.tint(GrayMedium) ) Spacer(modifier = Modifier.width(4.dp)) - Text(text = "Ithaca (Schoellkopf)", style = bodyNormal.copy(color = GrayPrimary)) + Text(text = gameCard.locationString, style = bodyNormal.copy(color = GrayPrimary)) Spacer(modifier = Modifier.width(12.dp)) - Image( - painter = painterResource(id = R.drawable.ic_location), - contentDescription = "Location Icon", + Icon( + painter = painterResource(id = R.drawable.ic_time), + contentDescription = "Time Icon", modifier = Modifier - .width(24.dp) - .height(24.dp), - colorFilter = ColorFilter.tint(GrayMedium) + .size(24.dp), + tint = GrayMedium ) Spacer(modifier = Modifier.width(4.dp)) - Text(text = "9/28/2024, 2:00PM", style = bodyNormal.copy(color = GrayPrimary)) + Text(text = gameCard.dateString, style = bodyNormal.copy(color = GrayPrimary)) } - //render the below if the game is in the future - Spacer(modifier = Modifier.height(40.dp)) - TimeUntilStartCard(2, 0) + // render the below if the game is in the future + // TODO: MESSY, is it every the case when there is a boxscore but no scoring summary + if (gameCard.isPastStartTime) { + if (gameCard.scoreBreakdown?.isNotEmpty() == true) { + Spacer(modifier = Modifier.height(24.dp)) + BoxScore(gameCard.gameData) + Spacer(modifier = Modifier.height(24.dp)) + } + if (gameCard.boxScore.isNotEmpty()) { + Text( + "Scoring Summary", fontSize = 18.sp, + style = heading2, + ) // TODO: NAVIGATION + Spacer(modifier = Modifier.height(16.dp)) + ScoringSummary(gameCard.scoreEvent) + } else { + Text("No Scoring Summary") // TODO: Make state when there are no scores + } + } else { + val context = LocalContext.current + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + + if (gameCard.daysUntilGame != null && gameCard.hoursUntilGame != null) { + TimeUntilStartCard( + gameCard.daysUntilGame, + gameCard.hoursUntilGame + ) + } + + Spacer(modifier = Modifier.weight(1f)) + ButtonPrimary( + "Add to Calendar", + painterResource(R.drawable.ic_calendar), + onClick = { + gameCard.toCalendarEvent()?.let { event -> + addToCalendar(context = context, event) + } + } + ) + } + + } } - Spacer(modifier = Modifier.height(84.dp)) - ButtonPrimary("Add to Calendar", painterResource(R.drawable.ic_calendar)) } } - @Preview @Composable -private fun GameDetailsScreenPreview() = ScorePreview { - GameDetailsScreen() -// import androidx.compose.ui.tooling.preview.Preview -// import androidx.compose.ui.unit.dp -// import com.cornellappdev.score.R -// import com.cornellappdev.score.model.ScoreEvent -// import com.cornellappdev.score.model.Team -// //TODO: Game Header, meta info -// @Composable -// fun GameDetailsScreen(scoreEvents: List, onArrowClick: () -> Unit) { -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .padding(16.dp) -// ) { -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 8.dp), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text( -// text = "Scoring Summary", -// style = MaterialTheme.typography.titleMedium, -// modifier = Modifier.weight(1f) -// ) - -// Icon( -// imageVector = Icons.Default.LocationOn, -// contentDescription = "Location Icon", -// modifier = Modifier -// .clickable { onArrowClick() } -// .padding(8.dp) -// ) -// } - -// LazyColumn( -// modifier = Modifier.fillMaxWidth() -// ) { -// items(scoreEvents.size) { event -> -// ScoreEventItem(event = scoreEvents[event]) -// Divider(color = Color.LightGray, thickness = 0.5.dp) -// } -// } -// } -// } - - -// @Preview(showBackground = true) -// @Composable -// fun PreviewGameDetailsScreen() { -// // Sample Team and ScoreEvent data -// val team1 = Team(name = "Cornell", logo = R.drawable.cornell_logo) -// val team2 = Team(name = "Yale", logo = R.drawable.yale_logo) - -// val scoreEvents = listOf( -// ScoreEvent( -// id = 1, -// time = "6:21", -// quarter = "1st Quarter", -// team = team1, -// eventType = "Field Goal", -// score = "10 - 7", -// description = "Zhao, Alan field goal attempt from 24 GOOD" -// ), -// ScoreEvent( -// id = 2, -// time = "8:40", -// quarter = "1st Quarter", -// team = team2, -// eventType = "Touchdown", -// score = "7 - 7", -// description = "McCaughey, Brogan right pass complete to Yates, Ry for 8 yards to the COROO, TOUCHDOWN. (Conforti, Nick kick attempt good.)" -// ), -// ScoreEvent( -// id = 3, -// time = "11:29", -// quarter = "1st Quarter", -// team = team1, -// eventType = "Touchdown", -// score = "7 - 0", -// description = "Wang, Jameson left pass complete to Lee, Brendan for 34 yards to the YALOO, TOUCHDOWN. (Zhao, Alan kick attempt good.)" -// ) -// ) - -// GameDetailsScreen( -// scoreEvents = scoreEvents, -// onArrowClick = { -// println("Arrow clicked to view more details") -// } -// ) -} \ No newline at end of file +private fun GameDetailsPreview() { + GameDetailsContent( + DetailsCardData( + title = "Championship Game", + opponentLogo = "https://example.com/logo.png", + opponent = "Wildcats", + opponentColor = Color(0xFF123456), + date = LocalDate.of(2025, 4, 20), + time = "7:30 PM", + dateString = "April 20, 2025", + isPastStartTime = false, + location = "Main Stadium", + locationString = "Main Stadium, Cityville", + gender = "Men's", + genderIcon = 123, // Dummy resource ID + sport = "Basketball", + sportIcon = 456, // Dummy resource ID + boxScore = listOf( + GameDetailsBoxScore( + team = "Tigers", + period = "1st", + time = "12:34", + description = "3-point shot", + scorer = "John Doe", + assist = "Mike Smith", + scoreBy = "Tigers", + corScore = 21, + oppScore = 18 + ), + GameDetailsBoxScore( + team = "Wildcats", + period = "1st", + time = "10:01", + description = "Layup", + scorer = "Jane Roe", + assist = "Tom Lee", + scoreBy = "Wildcats", + corScore = 21, + oppScore = 20 + ) + ), + scoreBreakdown = listOf( + listOf("10", "15", "20", "18"), // Tigers per quarter + listOf("12", "10", "18", "22") // Wildcats per quarter + ), + gameData = GameData( + Pair( + TeamScore( + team = TeamBoxScore( + name = "Tigers", + ), + scoresByPeriod = listOf(20, 18, 22, 18), + totalScore = 78 + ), + TeamScore( + team = TeamBoxScore( + name = "Wildcats", + ), + scoresByPeriod = listOf(18, 20, 16, 21), + totalScore = 75 + ) + ) + ), + scoreEvent = listOf( + ScoreEvent( + id = 1, + time = "11:11", + quarter = "2nd", + team = TeamGameSummary( + name = "Tigers", + logo = "https://example.com/tigers.png" + ), + eventType = "3PT", + score = "36-34", + description = "Three-pointer by John Doe" + ), + ScoreEvent( + id = 2, + time = "08:45", + quarter = "3rd", + team = TeamGameSummary( + name = "Wildcats", + logo = "https://example.com/wildcats.png" + ), + eventType = "FT", + score = "36-35", + description = "Free throw by Jane Roe" + ) + ), + daysUntilGame = 6, + hoursUntilGame = 144, + homeScore = 78, + oppScore = 75 + ) + ) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt index 09bad26..b1230b5 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt @@ -29,6 +29,8 @@ import com.cornellappdev.score.theme.Style.bodyNormal import com.cornellappdev.score.theme.Style.spanBodyNormal import com.cornellappdev.score.theme.White import com.cornellappdev.score.util.scoreEvents2 +import androidx.compose.foundation.layout.fillMaxSize +import coil3.compose.AsyncImage @Composable fun GameScoreSummaryScreenDetail(scoreEvents: List) { @@ -57,11 +59,10 @@ fun ScoreEventItemDetailed(event: ScoreEvent) { verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.SpaceBetween ) { - Image( - painter = painterResource(event.team.logo), + AsyncImage( + model = event.team.logo, contentDescription = event.team.name, - modifier = Modifier - .size(40.dp) + modifier = Modifier.size(40.dp) ) Spacer(modifier = Modifier.width(8.dp)) Column( 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 40fa9eb..f2a1918 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -44,7 +44,7 @@ import com.cornellappdev.score.viewmodel.HomeViewModel @Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel(), - navigateToGameDetails: (Boolean) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {} ) { val uiState = homeViewModel.collectUiStateValue() @@ -80,7 +80,7 @@ private fun HomeContent( uiState: HomeUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, - navigateToGameDetails: (Boolean) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {} ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { item { @@ -97,12 +97,14 @@ private fun HomeContent( Spacer(Modifier.height(16.dp)) } item { - GamesCarousel(uiState.upcomingGames) + GamesCarousel(uiState.upcomingGames, navigateToGameDetails) } stickyHeader { - Column(modifier = Modifier - .background(White) - .padding(horizontal = 24.dp)) { + Column( + modifier = Modifier + .background(White) + .padding(horizontal = 24.dp) + ) { Spacer(Modifier.height(24.dp)) Text( text = "Game Schedule", @@ -128,7 +130,7 @@ private fun HomeContent( } items(uiState.filteredGames) { val game = it - Column (modifier = Modifier.padding(horizontal = 24.dp)) { + Column(modifier = Modifier.padding(horizontal = 24.dp)) { GameCard( teamLogo = game.teamLogo, team = game.team, @@ -138,7 +140,7 @@ private fun HomeContent( sportIcon = painterResource(game.sportIcon), location = game.location, topCornerRound = true, - onClick = navigateToGameDetails + onClick = { navigateToGameDetails(game.id) } ) 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 d4f544b..e23a8bb 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -42,7 +42,7 @@ import com.cornellappdev.score.viewmodel.PastGamesViewModel @Composable fun PastGamesScreen( pastGamesViewModel: PastGamesViewModel = hiltViewModel(), - navigateToGameDetails: (Boolean) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {} ) { val uiState = pastGamesViewModel.collectUiStateValue() @@ -78,7 +78,7 @@ private fun PastGamesContent( uiState: PastGamesUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, - navigateToGameDetails: (Boolean) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {} ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { item { @@ -95,7 +95,7 @@ private fun PastGamesContent( Spacer(Modifier.height(16.dp)) } item { - GamesCarousel(uiState.pastGames) + GamesCarousel(uiState.pastGames, navigateToGameDetails) } stickyHeader { Column(modifier = Modifier @@ -129,7 +129,7 @@ private fun PastGamesContent( Column (modifier = Modifier.padding(horizontal = 24.dp)) { PastGameCard( data = game, - onClick = navigateToGameDetails + onClick = {navigateToGameDetails(game.id)} ) Spacer(modifier = Modifier.height(16.dp)) } diff --git a/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt b/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt new file mode 100644 index 0000000..24b02f1 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt @@ -0,0 +1,52 @@ +package com.cornellappdev.score.util + +import android.content.Context +import android.content.Intent +import android.provider.CalendarContract +import com.cornellappdev.score.model.DetailsCardData +import java.time.ZoneId + +data class CalendarEvent( + val title: String, + val description: String?, + val location: String?, + val date: java.time.LocalDate, + val time: String +) + +fun addToCalendar(context: Context, event: CalendarEvent) { + val startDateTime = event.date.atTime( + event.time.split(":").getOrNull(0)?.toIntOrNull() ?: 0, + event.time.split(":").getOrNull(1)?.toIntOrNull() ?: 0 + ) + + val startMillis = startDateTime + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + + val endMillis = startMillis + (2 * 60 * 60 * 1000) // Default 2-hour duration + + val intent = Intent(Intent.ACTION_INSERT).apply { + data = CalendarContract.Events.CONTENT_URI + putExtra(CalendarContract.Events.TITLE, event.title) + putExtra(CalendarContract.Events.EVENT_LOCATION, event.location) + putExtra(CalendarContract.Events.DESCRIPTION, event.description) + putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) + putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) + } + + context.startActivity(intent) +} + +fun DetailsCardData.toCalendarEvent(): CalendarEvent? { + val date = this.date ?: return null + + return CalendarEvent( + title = "Cornell vs. ${this.opponent}", + description = "${this.sport} game (${this.gender})", + location = this.locationString, + date = date, + time = this.time + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt index 0fe3def..4b5818b 100644 --- a/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt +++ b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt @@ -1,7 +1,11 @@ package com.cornellappdev.score.util +import android.util.Log +import java.time.Duration import java.time.LocalDate -import java.time.format.DateTimeFormatter; +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale /** * Converts date of form String "month-abbr day (day-of-week)" (for example, "Apr 29 (Tue)") to a LocalDate object @@ -23,3 +27,62 @@ fun parseDateOrNull(strDate: String): LocalDate? { */ val outputFormatter = DateTimeFormatter.ofPattern("M/d/yyyy") +/** + * Parses a date and time string into a LocalDateTime object. + * + * @param date the date string to parse, in the format "MMM d (EEE)" + * @param time the time string to parse, in the format "h:mm a", with or without periods + * @return a LocalDateTime object if parsing succeeds, or null if the format is invalid + */ +fun parseDateTimeOrNull(date: String, time: String): LocalDateTime? { + val subDate = date.substringBefore(" (") + date.substringAfter(")") + val cleanedTime = time + .replace(".", "") + .trim() + .uppercase() + + val dateTimeString = "$subDate $cleanedTime" + Log.d("parseDateTimeOrNull", "parseDateTimeOrNull: ${dateTimeString}") + val formatter = DateTimeFormatter.ofPattern("MMM d yyyy h:mm a", Locale.ENGLISH) + + return try { + LocalDateTime.parse(dateTimeString, formatter) + } catch (e: Exception) { + null + } +} + + +/** + * Formats a date and time string into a user-friendly display string. + * + * @param date the date string to parse, in the format "MMM d (EEE)" + * @param time the time string to parse, in the format "h:mm a", with or without periods + * @return a formatted date-time string in the format "MMM d, h:mma", or an empty string if parsing fails + */ +fun formatDateTimeDisplay(date: String, time: String): String { + val dateTime = parseDateTimeOrNull(date, time) + val formatter = DateTimeFormatter.ofPattern("MMM d, h:mma", Locale.ENGLISH) + + return dateTime?.format(formatter) ?: "" +} + +/** + * Calculates the time remaining until a specified start date and time. + * + * @param date the date string to parse, in the format "MMM d (EEE)" + * @param time the time string to parse, in the format "h:mm a", with or without periods + * @return a pair of (days, hours) until the start time, or null if parsing fails or the time is in the past + */ +fun getTimeUntilStart(date: String, time: String): Pair? { + val gameStart = parseDateTimeOrNull(date, time) ?: return null + val now = LocalDateTime.now() + + if (gameStart.isBefore(now)) return null + + val duration = Duration.between(now, gameStart) + val days = duration.toDays().toInt() + val hours = duration.minusDays(days.toLong()).toHours().toInt() + return days to hours +} + diff --git a/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt new file mode 100644 index 0000000..985b9b5 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt @@ -0,0 +1,104 @@ +package com.cornellappdev.score.util + +import com.cornellappdev.score.model.GameData +import com.cornellappdev.score.model.TeamBoxScore +import com.cornellappdev.score.model.TeamScore + +/** + * Converts a list of score strings into a list of integers by period and a total score. + * + * Handles null values and non-numeric values: + * - Nulls or "X" are 0 + * - If the sport is baseball, only the first 9 periods counted + * - For other sports, the last item in the list is treated as the total score. + * + * @param scoreList the list of score strings, where each item represents a period score and the last item may be the total + * @param sport the sport type, used to apply specific rules + * @return a pair where the first value is a list of parsed period scores and the second is the total score (or null if invalid) + */ +// TODO: ASK ABOUT OT. Other sports might be added. +fun convertScores(scoreList: List?, sport: String): Pair, Int?> { + if (scoreList == null || scoreList.size < 2) return Pair(emptyList(), null) + + var scoresByPeriod = scoreList + .subList(0, scoreList.size - 1) + .map { + when { + it == null -> 0 + it.uppercase() == "X" -> 0 + else -> it.toIntOrNull() ?: 0 + } + } + + if (sport.lowercase() == "baseball") { + scoresByPeriod = scoresByPeriod.take(9) + val totalScore = scoresByPeriod.sum() + return Pair(scoresByPeriod, totalScore) + } + + val totalScore = scoreList.last()?.toIntOrNull() + return Pair(scoresByPeriod, totalScore) +} + +/** + * Converts score breakdowns and team box scores into a GameData object. + * + * Uses convertScores to parse individual team scores. If a score breakdown is missing or invalid, + * returns empty scores and zero totals. + * + * @param scoreBreakdown a list containing two lists of score strings, one for each team + * @param team1 the first team's box score information + * @param team2 the second team's box score information + * @param sport the sport type, passed through to convertScores + * @return a GameData object containing structured scores for both teams + */ +fun toGameData( + scoreBreakdown: List?>?, + team1: TeamBoxScore, + team2: TeamBoxScore, + sport: String +): GameData { + val (team1Scores, team1Total) = scoreBreakdown?.getOrNull(0)?.let { + convertScores(it, sport) + } ?: (emptyList() to null) + + val (team2Scores, team2Total) = scoreBreakdown?.getOrNull(1)?.let { + convertScores(it, sport) + } ?: (emptyList() to null) + + val team1Score = + TeamScore(team = team1, scoresByPeriod = team1Scores, totalScore = team1Total ?: 0) + val team2Score = + TeamScore(team = team2, scoresByPeriod = team2Scores, totalScore = team2Total ?: 0) + + return GameData(teamScores = Pair(team1Score, team2Score)) +} + +/** + * Parses a result string into a pair of home and opponent scores. + * + * Expected format: "Some text,HOME-OPP", where HOME and OPP are integers. + * If the format is invalid or parsing fails, returns null. + * + * @param result the result string to parse, e.g., "L,3-2" + * @return a pair of (homeScore, oppScore) if parsing succeeds, or null if the format is invalid + */ +fun parseResultScore(result: String?): Pair? { + if (result.isNullOrBlank()) return null + + val parts = result.split(",") + if (parts.size != 2) return null + + val scorePart = parts[1].split("-") + if (scorePart.size != 2) return null + + val homeScore = scorePart[0].toIntOrNull() + val oppScore = scorePart[1].toIntOrNull() + + if (homeScore != null && oppScore != null) { + return Pair(homeScore, oppScore) + } else { + return null + } +} + 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 10fcbf7..02cd1b8 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -7,11 +7,13 @@ import com.cornellappdev.score.model.GameData import com.cornellappdev.score.model.ScoreEvent import com.cornellappdev.score.model.Sport import com.cornellappdev.score.model.SportSelection -import com.cornellappdev.score.model.Team +import com.cornellappdev.score.model.TeamBoxScore +import com.cornellappdev.score.model.TeamGameSummary import com.cornellappdev.score.model.TeamScore import java.time.LocalDate val PENN_GAME = GameCardData( + id = "", teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", team = "Penn", teamColor = Color(0x66B31B1B), @@ -27,6 +29,7 @@ val PENN_GAME = GameCardData( ) val PRINCETON_GAME = GameCardData( + id = "", teamLogo = "https://cornellbigred.com/images/logos/Princeton_Tigers.png?width=80&height=80&mode=max", team = "Princeton", teamColor = Color(0x66FF6000), @@ -51,8 +54,8 @@ val gameList = listOf( PRINCETON_GAME ) -val team1 = Team(name = "Cornell", R.drawable.cornell_logo) -val team2 = Team(name = "Yale", R.drawable.yale_logo) +val team1 = TeamBoxScore(name = "Cornell") +val team2 = TeamBoxScore(name = "Yale") val teamScore1 = TeamScore( team = team1, @@ -67,12 +70,20 @@ val teamScore2 = TeamScore( val gameData = GameData(teamScores = Pair(teamScore1, teamScore2)) +val team3 = TeamGameSummary( + name = "Cornell", + "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max" +) +val team4 = TeamGameSummary( + name = "Yale", + "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max" +) val scoreEvents1 = listOf( ScoreEvent( id = 1, time = "6:21", quarter = "1st Quarter", - team = team1, + team = team3, eventType = "Field Goal", score = "10 - 7" ), @@ -80,7 +91,7 @@ val scoreEvents1 = listOf( id = 2, time = "8:40", quarter = "1st Quarter", - team = team2, + team = team4, eventType = "Touchdown", score = "7 - 7" ), @@ -88,7 +99,7 @@ val scoreEvents1 = listOf( id = 3, time = "11:29", quarter = "1st Quarter", - team = team1, + team = team3, eventType = "Touchdown", score = "7 - 0" ) @@ -98,7 +109,7 @@ val scoreEvents2 = listOf( id = 1, time = "6:21", quarter = "1st Quarter", - team = team1, + team = team3, eventType = "Field Goal", score = "10 - 7", description = "Zhao, Alan field goal attempt from 24 GOOD" @@ -107,7 +118,7 @@ val scoreEvents2 = listOf( id = 2, time = "8:40", quarter = "1st Quarter", - team = team2, + team = team4, eventType = "Touchdown", score = "7 - 7", description = "McCaughey, Brogan right pass complete to Yates, Ry for 8 yards to the COROO, TOUCHDOWN. (Conforti, Nick kick attempt good.)" @@ -116,7 +127,7 @@ val scoreEvents2 = listOf( id = 3, time = "11:29", quarter = "1st Quarter", - team = team1, + team = team3, eventType = "Touchdown", score = "7 - 0", description = "Wang, Jameson left pass complete to Lee, Brendan for 34 yards to the YALOO, TOUCHDOWN. (Zhao, Alan kick attempt good.)" diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt index 3526a73..031f5d5 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt @@ -1,20 +1,42 @@ package com.cornellappdev.score.viewmodel -import com.cornellappdev.score.model.GameCardData -import com.cornellappdev.score.nav.root.RootNavigationRepository +import androidx.lifecycle.SavedStateHandle +import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.DetailsCardData +import com.cornellappdev.score.model.ScoreRepository +import com.cornellappdev.score.model.map +import com.cornellappdev.score.model.toGameCardData import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +data class GameDetailsUiState( + val loadedState: ApiResponse +) + @HiltViewModel class GameDetailsViewModel @Inject constructor( - private val rootNavigationRepository: RootNavigationRepository, - ) : BaseViewModel( - initialUiState = GameDetailsUiState( - homeScore = 0, awayScore = 0 + scoreRepository: ScoreRepository, + savedStateHandle: SavedStateHandle +) : BaseViewModel( + initialUiState = GameDetailsUiState( + loadedState = ApiResponse.Loading ) ) { - data class GameDetailsUiState( - val homeScore: Int, - val awayScore: Int, - ) + init { + val gameId: String? = savedStateHandle["gameId"] + gameId?.let { + scoreRepository.getGameById(it) + asyncCollect(scoreRepository.currentGamesFlow) { response -> + applyMutation { + copy( + loadedState = response.map { gameCard -> + gameCard.toGameCardData() + } + ) + } + } + } ?: applyMutation { + copy(loadedState = ApiResponse.Error) + } + } } \ No newline at end of file