fix: center top bar match details text and add error message component

This commit is contained in:
Leonardo Murça 2025-07-19 15:28:30 -03:00
parent c10a667b8d
commit fc496e9eed
3 changed files with 123 additions and 29 deletions

View file

@ -0,0 +1,66 @@
package xyz.leomurca.csgomatches.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun ErrorMessage(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
if (onRetry != null) {
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRetry,
colors = ButtonDefaults.buttonColors()
.copy(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text("Tentar novamente", style = MaterialTheme.typography.bodyMedium)
}
}
}
}

View file

@ -47,7 +47,7 @@ import xyz.leomurca.csgomatches.R
import xyz.leomurca.csgomatches.domain.model.MatchStatus import xyz.leomurca.csgomatches.domain.model.MatchStatus
import xyz.leomurca.csgomatches.domain.model.Player import xyz.leomurca.csgomatches.domain.model.Player
import xyz.leomurca.csgomatches.domain.model.Team import xyz.leomurca.csgomatches.domain.model.Team
import xyz.leomurca.csgomatches.ui.components.LoadingIndicator import xyz.leomurca.csgomatches.ui.components.ErrorMessage
import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute
import xyz.leomurca.csgomatches.ui.theme.LiveRed import xyz.leomurca.csgomatches.ui.theme.LiveRed
import xyz.leomurca.csgomatches.ui.theme.White import xyz.leomurca.csgomatches.ui.theme.White
@ -62,16 +62,7 @@ fun MatchDetailsScreen(
val uiState = viewModel.uiState.collectAsState() val uiState = viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (matchDetails.leftOpponentId != -1L) { loadTeams(matchDetails, viewModel)
viewModel.loadTeam(matchDetails.leftOpponentId.toString(), isLeft = true)
} else {
viewModel.applyPlaceholderTeamToSide(isLeft = true)
}
if (matchDetails.rightOpponentId != -1L) {
viewModel.loadTeam(matchDetails.rightOpponentId.toString(), isLeft = false)
} else {
viewModel.applyPlaceholderTeamToSide(isLeft = false)
}
} }
Column( Column(
@ -79,13 +70,15 @@ fun MatchDetailsScreen(
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background),
) { ) {
TopBar(matchDetails.leagueAndSerieName, onBackClick)
val value = uiState.value val value = uiState.value
when { when {
value.errorMessage != null -> Text(value.errorMessage) value.errorMessage != null -> ErrorMessage(
value.isLoading -> LoadingIndicator() message = value.errorMessage,
onRetry = { loadTeams(matchDetails, viewModel) }
)
value.leftTeam != null && value.rightTeam != null -> Column { value.leftTeam != null && value.rightTeam != null -> Column {
TopBar(matchDetails.leagueAndSerieName, onBackClick)
MatchupRow(leftTeam = value.leftTeam, rightTeam = value.rightTeam) MatchupRow(leftTeam = value.leftTeam, rightTeam = value.rightTeam)
ScheduleRow(matchDetails.scheduleString, matchDetails.matchStatus) ScheduleRow(matchDetails.scheduleString, matchDetails.matchStatus)
DualPlayerLists(value.leftTeam.players, value.rightTeam.players) DualPlayerLists(value.leftTeam.players, value.rightTeam.players)
@ -179,7 +172,9 @@ private fun PlayerAvatar(size: Dp, imageUrl: String?, nickName: String?) {
) )
if (isLoading) { if (isLoading) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(16.dp).align(Alignment.Center), modifier = Modifier
.size(16.dp)
.align(Alignment.Center),
strokeWidth = 2.dp, strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) )
@ -289,19 +284,30 @@ private fun MatchupRow(leftTeam: Team, rightTeam: Team) {
@Composable @Composable
private fun TopBar(leagueAndSerieName: String, onBackClick: () -> Unit) { private fun TopBar(leagueAndSerieName: String, onBackClick: () -> Unit) {
Row( Box(
Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(top = 52.dp, start = 24.dp, end = 24.dp), .padding(top = 52.dp)
verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton(onBackClick, modifier = Modifier) { IconButton(
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "", tint = White) onClick = onBackClick,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 18.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Back",
tint = White
)
} }
Text( Text(
leagueAndSerieName, text = leagueAndSerieName,
modifier = Modifier
.align(Alignment.Center)
.requiredWidthIn(max = 250.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = White, color = White,
@ -316,3 +322,20 @@ private fun playerFullNameOrEmpty(firstName: String?, lastName: String?): String
return if (fullName.isBlank()) "" else fullName return if (fullName.isBlank()) "" else fullName
} }
private fun loadTeams(matchDetails: MatchDetailsRoute, viewModel: MatchDetailsViewModel) {
loadTeamOrPlaceholder(matchDetails.leftOpponentId, isLeft = true, viewModel)
loadTeamOrPlaceholder(matchDetails.rightOpponentId, isLeft = false, viewModel)
}
private fun loadTeamOrPlaceholder(
opponentId: Long,
isLeft: Boolean,
viewModel: MatchDetailsViewModel
) {
if (opponentId != -1L) {
viewModel.loadTeam(opponentId.toString(), isLeft)
} else {
viewModel.applyPlaceholderTeamToSide(isLeft)
}
}

View file

@ -16,12 +16,12 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import xyz.leomurca.csgomatches.domain.model.Match import xyz.leomurca.csgomatches.domain.model.Match
import xyz.leomurca.csgomatches.ui.components.ErrorMessage
import xyz.leomurca.csgomatches.ui.components.MatchCard import xyz.leomurca.csgomatches.ui.components.MatchCard
import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute
import xyz.leomurca.csgomatches.ui.screens.matches.MatchesViewModel.MatchesUiState import xyz.leomurca.csgomatches.ui.screens.matches.MatchesViewModel.MatchesUiState
@ -37,7 +37,8 @@ fun MatchesScreen(
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
@ -53,7 +54,8 @@ fun MatchesScreen(
scrolledContainerColor = MaterialTheme.colorScheme.background scrolledContainerColor = MaterialTheme.colorScheme.background
) )
) )
}) { innerPadding -> }
) { innerPadding ->
PullToRefreshBox( PullToRefreshBox(
isRefreshing = uiState.value is MatchesUiState.Loading, isRefreshing = uiState.value is MatchesUiState.Loading,
onRefresh = { viewModel.loadUpcomingMatches() }, onRefresh = { viewModel.loadUpcomingMatches() },
@ -61,12 +63,15 @@ fun MatchesScreen(
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) { ) {
when (val value = uiState.value) { when (val value = uiState.value) {
is MatchesUiState.Success -> MatchesList(value.matches, onTapCard) is MatchesUiState.Success -> MatchesList(value.matches, onTapCard)
is MatchesUiState.Error -> Text(value.message) is MatchesUiState.Error -> ErrorMessage(
else -> Unit // Do nothing message = value.message,
onRetry = { viewModel.loadUpcomingMatches() }
)
else -> Unit
} }
} }
} }