From c5d1bbcbed9e8ecf06af0bbc70fcf4b1f4b10657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Mur=C3=A7a?= Date: Fri, 18 Jul 2025 18:08:42 -0300 Subject: [PATCH] feat: format ScheduleBadge correctly based on local time zone --- .idea/misc.xml | 1 - .../data/local/MatchLocalDataSource.kt | 10 +-- .../csgomatches/data/mapper/MatchMapper.kt | 8 +- .../csgomatches/data/model/MatchDto.kt | 5 +- .../serializer/ZonedDateTimeSerializer.kt | 27 +++++++ .../data/remote/MatchesApiService.kt | 7 +- .../leomurca/csgomatches/di/NetworkModule.kt | 2 +- .../csgomatches/domain/model/Match.kt | 6 +- .../csgomatches/domain/model/MatchStatus.kt | 7 ++ .../csgomatches/ui/components/MatchCard.kt | 77 ++++++++++++++----- .../ui/screens/matches/MatchesScreen.kt | 2 +- .../leomurca/csgomatches/utils/Extensions.kt | 52 +++++++++++++ 12 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/xyz/leomurca/csgomatches/data/model/serializer/ZonedDateTimeSerializer.kt create mode 100644 app/src/main/java/xyz/leomurca/csgomatches/domain/model/MatchStatus.kt create mode 100644 app/src/main/java/xyz/leomurca/csgomatches/utils/Extensions.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639..b2c751a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/java/xyz/leomurca/csgomatches/data/local/MatchLocalDataSource.kt b/app/src/main/java/xyz/leomurca/csgomatches/data/local/MatchLocalDataSource.kt index 36ecfb8..71b8545 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/data/local/MatchLocalDataSource.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/data/local/MatchLocalDataSource.kt @@ -14,7 +14,7 @@ class MatchLocalDataSource : MatchDataSource { data = listOf( // Happy path MatchDto( - beginAt = "2025-07-21T10:30:00Z", + beginAt = null, opponents = listOf( OpponentDto( type = "team", @@ -46,7 +46,7 @@ class MatchLocalDataSource : MatchDataSource { ), // Empty Opponents MatchDto( - beginAt = "2025-07-27T10:30:00Z", + beginAt = null, opponents = emptyList(), league = LeagueDto( id = 5078, @@ -62,7 +62,7 @@ class MatchLocalDataSource : MatchDataSource { // Only 1 opponent MatchDto( - beginAt = "2025-07-21T10:30:00Z", + beginAt = null, opponents = listOf( OpponentDto( type = "team", @@ -86,7 +86,7 @@ class MatchLocalDataSource : MatchDataSource { ), // 1 opponent without logo MatchDto( - beginAt = "2025-07-21T10:30:00Z", + beginAt = null, opponents = listOf( OpponentDto( type = "team", @@ -118,7 +118,7 @@ class MatchLocalDataSource : MatchDataSource { ), // League without logo MatchDto( - beginAt = "2025-07-21T10:30:00Z", + beginAt = null, opponents = listOf( OpponentDto( type = "team", diff --git a/app/src/main/java/xyz/leomurca/csgomatches/data/mapper/MatchMapper.kt b/app/src/main/java/xyz/leomurca/csgomatches/data/mapper/MatchMapper.kt index ea7d93b..7f57069 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/data/mapper/MatchMapper.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/data/mapper/MatchMapper.kt @@ -3,12 +3,13 @@ package xyz.leomurca.csgomatches.data.mapper import xyz.leomurca.csgomatches.data.model.MatchDto import xyz.leomurca.csgomatches.domain.model.League import xyz.leomurca.csgomatches.domain.model.Match +import xyz.leomurca.csgomatches.domain.model.MatchStatus import xyz.leomurca.csgomatches.domain.model.Opponent import xyz.leomurca.csgomatches.domain.model.Serie fun MatchDto.toDomain(): Match { return Match( - beginAt = beginAt ?: "", + beginAt = beginAt, opponents = opponents.map { op -> Opponent( id = op.opponent.id, @@ -25,6 +26,9 @@ fun MatchDto.toDomain(): Match { id = serie.id, name = serie.fullName ?: "" ), - status = status ?: "" + status = status?.toMatchStatus() ?: MatchStatus.UNKNOWN ) } + +private fun String?.toMatchStatus() = + if (this == "running") MatchStatus.LIVE else MatchStatus.SCHEDULED \ No newline at end of file diff --git a/app/src/main/java/xyz/leomurca/csgomatches/data/model/MatchDto.kt b/app/src/main/java/xyz/leomurca/csgomatches/data/model/MatchDto.kt index 9152ab6..43c78a4 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/data/model/MatchDto.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/data/model/MatchDto.kt @@ -2,11 +2,14 @@ package xyz.leomurca.csgomatches.data.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import xyz.leomurca.csgomatches.data.model.serializer.ZonedDateTimeSerializer +import java.time.ZonedDateTime @Serializable data class MatchDto( + @Serializable(with = ZonedDateTimeSerializer::class) @SerialName("begin_at") - val beginAt: String?, + val beginAt: ZonedDateTime?, val opponents: List, val league: LeagueDto, val serie: SerieDto, diff --git a/app/src/main/java/xyz/leomurca/csgomatches/data/model/serializer/ZonedDateTimeSerializer.kt b/app/src/main/java/xyz/leomurca/csgomatches/data/model/serializer/ZonedDateTimeSerializer.kt new file mode 100644 index 0000000..ba21372 --- /dev/null +++ b/app/src/main/java/xyz/leomurca/csgomatches/data/model/serializer/ZonedDateTimeSerializer.kt @@ -0,0 +1,27 @@ +package xyz.leomurca.csgomatches.data.model.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime + +object ZonedDateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ZonedDateTime) { + encoder.encodeString(value.withZoneSameInstant(ZoneOffset.UTC).toString()) + } + + override fun deserialize(decoder: Decoder): ZonedDateTime { + val isoString = decoder.decodeString() + val instant = Instant.parse(isoString) + return instant.atZone(ZoneId.systemDefault()) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/leomurca/csgomatches/data/remote/MatchesApiService.kt b/app/src/main/java/xyz/leomurca/csgomatches/data/remote/MatchesApiService.kt index 3c4aad9..7b40939 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/data/remote/MatchesApiService.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/data/remote/MatchesApiService.kt @@ -2,10 +2,15 @@ package xyz.leomurca.csgomatches.data.remote import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.Query import xyz.leomurca.csgomatches.data.model.MatchDto interface MatchesApiService { @GET("matches") - suspend fun upcomingMatches(): Response> + suspend fun upcomingMatches( + @Query("filter[status]") filterStatus: String = "running, not_started", + @Query("finished") finished: Boolean = false, + @Query("sort") sort: String = "begin_at", + ): Response> } \ No newline at end of file diff --git a/app/src/main/java/xyz/leomurca/csgomatches/di/NetworkModule.kt b/app/src/main/java/xyz/leomurca/csgomatches/di/NetworkModule.kt index 0a63b70..d29969d 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/di/NetworkModule.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/di/NetworkModule.kt @@ -70,7 +70,7 @@ internal object NetworkModule { matchesApiService: MatchesApiService, json: Json ): MatchDataSource { - val useRemote = false + val useRemote = true return if (useRemote) { MatchRemoteDataSourceImpl(matchesApiService, json) } else { diff --git a/app/src/main/java/xyz/leomurca/csgomatches/domain/model/Match.kt b/app/src/main/java/xyz/leomurca/csgomatches/domain/model/Match.kt index d1e017d..cf62608 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/domain/model/Match.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/domain/model/Match.kt @@ -1,10 +1,12 @@ package xyz.leomurca.csgomatches.domain.model +import java.time.ZonedDateTime + data class Match( - val beginAt: String, + val beginAt: ZonedDateTime?, val opponents: List, val league: League, val serie: Serie, - val status: String + val status: MatchStatus ) \ No newline at end of file diff --git a/app/src/main/java/xyz/leomurca/csgomatches/domain/model/MatchStatus.kt b/app/src/main/java/xyz/leomurca/csgomatches/domain/model/MatchStatus.kt new file mode 100644 index 0000000..cd50a2b --- /dev/null +++ b/app/src/main/java/xyz/leomurca/csgomatches/domain/model/MatchStatus.kt @@ -0,0 +1,7 @@ +package xyz.leomurca.csgomatches.domain.model + +enum class MatchStatus { + LIVE, + SCHEDULED, + UNKNOWN, +} diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt index 0865ef5..0470596 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt @@ -30,31 +30,30 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import xyz.leomurca.csgomatches.R import xyz.leomurca.csgomatches.domain.model.League +import xyz.leomurca.csgomatches.domain.model.Match +import xyz.leomurca.csgomatches.domain.model.MatchStatus import xyz.leomurca.csgomatches.domain.model.Opponent import xyz.leomurca.csgomatches.ui.theme.LiveRed import xyz.leomurca.csgomatches.ui.theme.White_20 import xyz.leomurca.csgomatches.ui.theme.White_50 +import xyz.leomurca.csgomatches.utils.toFormattedMatchTime +import java.time.ZonedDateTime @Composable fun MatchCard( - opponents: List, - league: League, - serieName: String, + match: Match, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier - .fillMaxWidth() - ) { + Box(modifier = modifier.fillMaxWidth()) { Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) ) { - MatchupRow(opponents) - LeagueInfoRow(league, serieName) + MatchupRow(match.opponents) + LeagueInfoRow(match.league, match.serie.name) } - ScheduleBadge() + ScheduleBadge(match.status, match.beginAt) } } @@ -140,17 +139,52 @@ private fun LeagueInfoRow(league: League, serieName: String) { } @Composable -private fun BoxScope.ScheduleBadge() { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp)) - .background(LiveRed) - .padding(8.dp) - ) { - Text( - text = "AGORA", style = MaterialTheme.typography.displayMedium, color = Color.White - ) +private fun BoxScope.ScheduleBadge(status: MatchStatus, beginAt: ZonedDateTime?) { + when (status) { + MatchStatus.LIVE -> + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp)) + .background(LiveRed) + .padding(8.dp) + ) { + Text( + text = "AGORA", + style = MaterialTheme.typography.displayMedium, + color = Color.White + ) + } + + MatchStatus.SCHEDULED -> + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp)) + .background(White_20) + .padding(8.dp) + ) { + Text( + text = beginAt.toFormattedMatchTime(), + style = MaterialTheme.typography.displayMedium, + color = Color.White + ) + } + + MatchStatus.UNKNOWN -> + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp)) + .background(White_20) + .padding(8.dp) + ) { + Text( + text = "A ser definido", + style = MaterialTheme.typography.displayMedium, + color = Color.White + ) + } } } @@ -174,3 +208,4 @@ private fun getOrDefaultOpponents(opponents: List): Pair default to default } } + diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matches/MatchesScreen.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matches/MatchesScreen.kt index 2b3c902..7b0482d 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matches/MatchesScreen.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matches/MatchesScreen.kt @@ -76,7 +76,7 @@ private fun MatchesList(matches: List) { contentPadding = PaddingValues(vertical = 24.dp) ) { items(matches) { - MatchCard(it.opponents, it.league, it.serie.name) + MatchCard(it) } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/leomurca/csgomatches/utils/Extensions.kt b/app/src/main/java/xyz/leomurca/csgomatches/utils/Extensions.kt new file mode 100644 index 0000000..b337397 --- /dev/null +++ b/app/src/main/java/xyz/leomurca/csgomatches/utils/Extensions.kt @@ -0,0 +1,52 @@ +package xyz.leomurca.csgomatches.utils + +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +/** + * Formats a [ZonedDateTime] instance into a user-friendly, localized string representing the match time. + * + * The formatting rules are as follows: + * - If the date is today, returns: `"Hoje, HH:mm"` + * - If the date is within the current week, returns: `"EEE, HH:mm"` (e.g., `"Ter, 22:00"`) + * - Otherwise, returns: `"dd.MM HH:mm"` (e.g., `"22.04 15:00"`) + * + * The day of the week is formatted in Portuguese (`pt-BR`) and its first letter is capitalized. + * + * @receiver the [ZonedDateTime] instance to format. If null, an empty string is returned. + * @return a formatted string representing the match time, or `"A definir"` if the input is null. + */ +fun ZonedDateTime?.toFormattedMatchTime(): String { + if (this == null) return "A definir" + + val targetDate = toLocalDate() + val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") + + return when { + targetDate.isToday() -> "Hoje, ${format(timeFormatter)}" + targetDate.isInCurrentWeek() -> { + val dayOfWeekFormatter = DateTimeFormatter.ofPattern("EEE", Locale("pt", "BR")) + val day = format(dayOfWeekFormatter).replaceFirstChar { + it.titlecase(Locale("pt", "BR")) + } + "$day, ${format(timeFormatter)}" + } + + else -> { + val fullFormatter = DateTimeFormatter.ofPattern("dd.MM HH:mm") + format(fullFormatter) + } + } +} + +private fun LocalDate.isToday() = isEqual(ZonedDateTime.now().toLocalDate()) + +private fun LocalDate.isInCurrentWeek(): Boolean { + val today = LocalDate.now() + val startOfWeek = today.with(DayOfWeek.MONDAY) + val endOfWeek = today.with(DayOfWeek.SUNDAY) + return !this.isBefore(startOfWeek) && !this.isAfter(endOfWeek) +} \ No newline at end of file