From 29a29852949fed68aa7da7485b2deafbca5cfffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Mur=C3=A7a?= Date: Fri, 18 Jul 2025 14:51:15 -0300 Subject: [PATCH] feat: add MatchCard initial structure --- app/build.gradle.kts | 2 + .../data/local/MatchLocalDataSource.kt | 120 ++++++++++-- .../csgomatches/ui/components/MatchCard.kt | 176 ++++++++++++++++++ .../ui/screens/matches/MatchesScreen.kt | 4 +- .../leomurca/csgomatches/ui/theme/Color.kt | 5 +- .../leomurca/csgomatches/ui/theme/Theme.kt | 1 + .../xyz/leomurca/csgomatches/ui/theme/Type.kt | 7 + .../res/drawable/fallback_image_round.xml | 5 + gradle/libs.versions.toml | 5 +- 9 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt create mode 100644 app/src/main/res/drawable/fallback_image_round.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3205221..c5db018 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,6 +70,8 @@ dependencies { implementation(libs.retrofit.kotlinx.serialization.converter) implementation(libs.kotlinx.serialization.json) implementation(libs.okhttp) + implementation(libs.coil.compose) + implementation(libs.coil.network) ksp(libs.hilt.android.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) 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 91a7528..36ecfb8 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 @@ -12,20 +12,7 @@ class MatchLocalDataSource : MatchDataSource { override suspend fun upcomingMatches(): Resource> { return Resource.Success( data = listOf( - MatchDto( - beginAt = "2025-07-27T10:30:00Z", - opponents = emptyList(), - league = LeagueDto( - id = 5078, - name = "United21", - imageUrl = "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png" - ), - serie = SerieDto( - id = 9519, - fullName = "Season 35 2025" - ), - status = "not_started" - ), + // Happy path MatchDto( beginAt = "2025-07-21T10:30:00Z", opponents = listOf( @@ -57,8 +44,111 @@ class MatchLocalDataSource : MatchDataSource { ), status = "not_started" ), + // Empty Opponents + MatchDto( + beginAt = "2025-07-27T10:30:00Z", + opponents = emptyList(), + league = LeagueDto( + id = 5078, + name = "United21", + imageUrl = "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png" + ), + serie = SerieDto( + id = 9519, + fullName = "Season 35 2025" + ), + status = "not_started" + ), - ) + // Only 1 opponent + MatchDto( + beginAt = "2025-07-21T10:30:00Z", + opponents = listOf( + OpponentDto( + type = "team", + opponent = OpponentRecord( + id = 128519, + name = "Bushido Wildcats", + imageUrl = "https://cdn.pandascore.co/images/team/image/136934/bushido_wildcatslogo_square.png" + ) + ), + ), + league = LeagueDto( + id = 5078, + name = "Gamers Club Liga Série A", + imageUrl = "https://cdn.pandascore.co/images/league/image/4554/Liga_Gamers_Club_SSrie_A_logo.png" + ), + serie = SerieDto( + id = 9519, + fullName = "July 2025" + ), + status = "not_started" + ), + // 1 opponent without logo + MatchDto( + beginAt = "2025-07-21T10:30:00Z", + opponents = listOf( + OpponentDto( + type = "team", + opponent = OpponentRecord( + id = 128519, + name = "NO ORG", + imageUrl = null + ) + ), + OpponentDto( + type = "team", + opponent = OpponentRecord( + id = 128519, + name = "Bushido Wildcats", + imageUrl = "https://cdn.pandascore.co/images/team/image/136934/bushido_wildcatslogo_square.png" + ) + ), + ), + league = LeagueDto( + id = 5078, + name = "Gamers Club Liga Série A", + imageUrl = "https://cdn.pandascore.co/images/league/image/4554/Liga_Gamers_Club_SSrie_A_logo.png" + ), + serie = SerieDto( + id = 9519, + fullName = "July 2025" + ), + status = "not_started" + ), + // League without logo + MatchDto( + beginAt = "2025-07-21T10:30:00Z", + opponents = listOf( + OpponentDto( + type = "team", + opponent = OpponentRecord( + id = 128519, + name = "Abyss team", + imageUrl = "https://cdn.pandascore.co/images/team/image/136868/146px_abyss_team_lightmode.png" + ) + ), + OpponentDto( + type = "team", + opponent = OpponentRecord( + id = 128519, + name = "Bushido Wildcats", + imageUrl = "https://cdn.pandascore.co/images/team/image/136934/bushido_wildcatslogo_square.png" + ) + ), + ), + league = LeagueDto( + id = 5078, + name = "ESEA", + imageUrl = null + ), + serie = SerieDto( + id = 9519, + fullName = "July 2025" + ), + status = "not_started" + ), + ) ) } } \ No newline at end of file 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 new file mode 100644 index 0000000..0865ef5 --- /dev/null +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt @@ -0,0 +1,176 @@ +package xyz.leomurca.csgomatches.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +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.Opponent +import xyz.leomurca.csgomatches.ui.theme.LiveRed +import xyz.leomurca.csgomatches.ui.theme.White_20 +import xyz.leomurca.csgomatches.ui.theme.White_50 + +@Composable +fun MatchCard( + opponents: List, + league: League, + serieName: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + MatchupRow(opponents) + LeagueInfoRow(league, serieName) + } + ScheduleBadge() + } +} + +@Composable +private fun MatchupRow(opponents: List) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 43.5.dp, bottom = 18.5.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + val (leftOpponent, rightOpponent) = getOrDefaultOpponents(opponents) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + AsyncImage( + model = leftOpponent.imageUrl, + contentDescription = "${leftOpponent.name} logo", + error = painterResource(R.drawable.fallback_image_round), + placeholder = painterResource(R.drawable.fallback_image_round), + modifier = Modifier.size(60.dp), + contentScale = ContentScale.Fit + ) + Text( + leftOpponent.name, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(top = 10.dp) + ) + } + + Text( + "vs", + style = MaterialTheme.typography.bodyMedium, + color = White_50, + modifier = Modifier.padding(horizontal = 20.dp) + ) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + AsyncImage( + model = rightOpponent.imageUrl, + contentDescription = "${rightOpponent.name} logo", + error = painterResource(R.drawable.fallback_image_round), + placeholder = painterResource(R.drawable.fallback_image_round), + modifier = Modifier.size(60.dp), + contentScale = ContentScale.Fit + ) + Text( + rightOpponent.name, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(top = 10.dp) + ) + } + } +} + +@Composable +private fun LeagueInfoRow(league: League, serieName: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .topBorder(White_20, 1.dp) + .padding(vertical = 8.dp, horizontal = 16.dp) + ) { + AsyncImage( + model = league.imageUrl, + contentDescription = "${league.name} logo", + error = painterResource(R.drawable.fallback_image_round), + placeholder = painterResource(R.drawable.fallback_image_round), + onLoading = { + }, + modifier = Modifier + .height(16.dp) + .wrapContentWidth(), + contentScale = ContentScale.Fit + ) + Text( + "${league.name} + $serieName", + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(start = 8.dp) + ) + } +} + +@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 Modifier.topBorder(color: Color, thickness: Dp): Modifier = this.then( + Modifier.drawBehind { + val strokeWidth = thickness.toPx() + drawLine( + color = color, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = strokeWidth + ) + } +) + +private fun getOrDefaultOpponents(opponents: List): Pair { + val default = Opponent(id = 0, name = "A ser definido", imageUrl = "") + return when { + opponents.size >= 2 -> opponents[0] to opponents[1] + opponents.size == 1 -> opponents[0] to default + else -> 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 4ae33f5..c691d4d 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 @@ -13,7 +13,9 @@ import androidx.compose.ui.unit.sp @Composable fun MatchesScreen() { Box( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center ) { Text("Main Screen", fontSize = 24.sp) diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Color.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Color.kt index 858c574..b58aa6a 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Color.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Color.kt @@ -4,4 +4,7 @@ import androidx.compose.ui.graphics.Color val White = Color(0xFFFFFFFF) val DeepSpaceBlue = Color(0xFF161621) -val StormySlate = Color(0xFF272639) \ No newline at end of file +val StormySlate = Color(0xFF272639) +val LiveRed = Color(0xFFF42A35) +val White_50 = Color(0x80FFFFFF) +val White_20 = Color(0x33FFFFFF) diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Theme.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Theme.kt index 8c4e65f..d2d23f9 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Theme.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Theme.kt @@ -8,6 +8,7 @@ private val AppColorScheme = lightColorScheme( background = DeepSpaceBlue, primary = White, secondary = StormySlate, + surface = StormySlate ) @Composable diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Type.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Type.kt index fdfceb1..c2a9877 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Type.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/theme/Type.kt @@ -65,4 +65,11 @@ val Typography = Typography( lineHeight = 14.sp, letterSpacing = 0.sp, ), // Nickname + displayMedium = TextStyle( + fontFamily = robotoFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 8.sp, + lineHeight = 8.sp, + letterSpacing = 0.sp, + ), // AGORA ) \ No newline at end of file diff --git a/app/src/main/res/drawable/fallback_image_round.xml b/app/src/main/res/drawable/fallback_image_round.xml new file mode 100644 index 0000000..9a3e29f --- /dev/null +++ b/app/src/main/res/drawable/fallback_image_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2181ece..1a6e83c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ retrofit = "1.0.0" retrofit2 = "2.11.0" kotlinxSerializationJson = "1.8.1" okhttp = "4.12.0" +coil = "3.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -39,7 +40,9 @@ androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscree retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } retrofit-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofit" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } +coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }