feat: add MatchCard initial structure

This commit is contained in:
Leonardo Murça 2025-07-18 14:51:15 -03:00
parent 3fd7e78090
commit 29a2985294
9 changed files with 307 additions and 18 deletions

View file

@ -70,6 +70,8 @@ dependencies {
implementation(libs.retrofit.kotlinx.serialization.converter) implementation(libs.retrofit.kotlinx.serialization.converter)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.coil.compose)
implementation(libs.coil.network)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View file

@ -12,20 +12,7 @@ class MatchLocalDataSource : MatchDataSource {
override suspend fun upcomingMatches(): Resource<List<MatchDto>> { override suspend fun upcomingMatches(): Resource<List<MatchDto>> {
return Resource.Success( return Resource.Success(
data = listOf( data = listOf(
MatchDto( // Happy path
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"
),
MatchDto( MatchDto(
beginAt = "2025-07-21T10:30:00Z", beginAt = "2025-07-21T10:30:00Z",
opponents = listOf( opponents = listOf(
@ -57,8 +44,111 @@ class MatchLocalDataSource : MatchDataSource {
), ),
status = "not_started" 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"
),
)
) )
} }
} }

View file

@ -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<Opponent>,
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<Opponent>) {
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<Opponent>): Pair<Opponent, Opponent> {
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
}
}

View file

@ -13,7 +13,9 @@ import androidx.compose.ui.unit.sp
@Composable @Composable
fun MatchesScreen() { fun MatchesScreen() {
Box( Box(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Main Screen", fontSize = 24.sp) Text("Main Screen", fontSize = 24.sp)

View file

@ -5,3 +5,6 @@ import androidx.compose.ui.graphics.Color
val White = Color(0xFFFFFFFF) val White = Color(0xFFFFFFFF)
val DeepSpaceBlue = Color(0xFF161621) val DeepSpaceBlue = Color(0xFF161621)
val StormySlate = Color(0xFF272639) val StormySlate = Color(0xFF272639)
val LiveRed = Color(0xFFF42A35)
val White_50 = Color(0x80FFFFFF)
val White_20 = Color(0x33FFFFFF)

View file

@ -8,6 +8,7 @@ private val AppColorScheme = lightColorScheme(
background = DeepSpaceBlue, background = DeepSpaceBlue,
primary = White, primary = White,
secondary = StormySlate, secondary = StormySlate,
surface = StormySlate
) )
@Composable @Composable

View file

@ -65,4 +65,11 @@ val Typography = Typography(
lineHeight = 14.sp, lineHeight = 14.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), // Nickname ), // Nickname
displayMedium = TextStyle(
fontFamily = robotoFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 8.sp,
lineHeight = 8.sp,
letterSpacing = 0.sp,
), // AGORA
) )

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="61.5dp" android:viewportHeight="15.875" android:viewportWidth="15.485" android:width="59.989136dp">
<path android:fillColor="#c4c4c4" android:fillType="nonZero" android:pathData="M-0,7.938a7.743,7.938 0,1 0,15.485 0a7.743,7.938 0,1 0,-15.485 0z" android:strokeColor="#00000000" android:strokeWidth="0.0297729"/>
</vector>

View file

@ -16,6 +16,7 @@ retrofit = "1.0.0"
retrofit2 = "2.11.0" retrofit2 = "2.11.0"
kotlinxSerializationJson = "1.8.1" kotlinxSerializationJson = "1.8.1"
okhttp = "4.12.0" okhttp = "4.12.0"
coil = "3.2.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } 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" } 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" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }