From 58d788f58fa4640102033a7aa9ed21fa6503ee81 Mon Sep 17 00:00:00 2001 From: "arthur.valin" Date: Tue, 30 Jan 2024 14:41:41 +0100 Subject: [PATCH] Add bet confirmation screen --- .../fr/iut/alldev/allin/test/mock/Bets.kt | 81 ++-- .../vo/bet/displayer/BetVOTestVisitor.kt | 10 +- .../java/fr/iut/alldev/allin/ext/FieldExt.kt | 3 + .../iut/alldev/allin/ui/bet/BetViewModel.kt | 30 +- .../BetConfirmationBottomSheet.kt | 388 ++++++++++++++++++ .../allin/ui/betCreation/BetCreationScreen.kt | 4 - .../ui/betCreation/BetCreationViewModel.kt | 21 +- .../ui/betResult/BetResultBottomSheet.kt | 25 +- .../betStatus/components/YesNoDetailsLine.kt | 35 +- .../vo/BetStatusBottomSheetBetDisplayer.kt | 7 +- .../alldev/allin/ui/core/AllInMarqueeBox.kt | 65 +++ .../fr/iut/alldev/allin/ui/main/MainScreen.kt | 19 +- .../iut/alldev/allin/ui/main/MainViewModel.kt | 40 ++ .../ui/preview/BetDetailPreviewProvider.kt | 2 +- src/app/src/main/res/values-fr/strings.xml | 1 + src/app/src/main/res/values/strings.xml | 1 + .../allin/data/repository/BetRepository.kt | 2 +- .../data/repository/impl/BetRepositoryImpl.kt | 6 +- 18 files changed, 590 insertions(+), 150 deletions(-) create mode 100644 src/app/src/main/java/fr/iut/alldev/allin/ui/betConfirmation/BetConfirmationBottomSheet.kt create mode 100644 src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInMarqueeBox.kt diff --git a/src/app/src/androidTest/java/fr/iut/alldev/allin/test/mock/Bets.kt b/src/app/src/androidTest/java/fr/iut/alldev/allin/test/mock/Bets.kt index 846c540..9b38b75 100644 --- a/src/app/src/androidTest/java/fr/iut/alldev/allin/test/mock/Bets.kt +++ b/src/app/src/androidTest/java/fr/iut/alldev/allin/test/mock/Bets.kt @@ -1,49 +1,48 @@ package fr.iut.alldev.allin.test.mock -import fr.iut.alldev.allin.data.model.bet.BetStatus -import fr.iut.alldev.allin.data.model.bet.CustomBet -import fr.iut.alldev.allin.data.model.bet.MatchBet -import fr.iut.alldev.allin.data.model.bet.YesNoBet -import java.time.ZonedDateTime +import fr.iut.alldev.allin.data.model.bet.vo.BetDetail object Bets { val bets by lazy { - listOf( - YesNoBet( - theme = "Theme", - phrase = "Phrase", - endRegisterDate = ZonedDateTime.now(), - endBetDate = ZonedDateTime.now(), - isPublic = true, - betStatus = BetStatus.InProgress, - creator = "creator", - ), - MatchBet( - theme = "Theme", - phrase = "Phrase", - endRegisterDate = ZonedDateTime.now(), - endBetDate = ZonedDateTime.now(), - isPublic = true, - betStatus = BetStatus.InProgress, - nameTeam1 = "Team_1", - nameTeam2 = "Team_2", - creator = "creator" - ), - CustomBet( - theme = "Theme", - phrase = "Phrase", - endRegisterDate = ZonedDateTime.now(), - endBetDate = ZonedDateTime.now(), - isPublic = true, - betStatus = BetStatus.InProgress, - creator = "creator", - possibleAnswers = listOf( - "Answer 1", - "Answer 2", - "Answer 3", - "Answer 4" - ) - ), + listOf( + /* YesNoBet( + theme = "Theme", + phrase = "Phrase", + endRegisterDate = ZonedDateTime.now(), + endBetDate = ZonedDateTime.now(), + isPublic = true, + betStatus = BetStatus.InProgress, + creator = "creator", + id = "" + ), + MatchBet( + theme = "Theme", + phrase = "Phrase", + endRegisterDate = ZonedDateTime.now(), + endBetDate = ZonedDateTime.now(), + isPublic = true, + betStatus = BetStatus.InProgress, + nameTeam1 = "Team_1", + nameTeam2 = "Team_2", + creator = "creator", + id = "" + ), + CustomBet( + theme = "Theme", + phrase = "Phrase", + endRegisterDate = ZonedDateTime.now(), + endBetDate = ZonedDateTime.now(), + isPublic = true, + betStatus = BetStatus.InProgress, + creator = "creator", + possibleAnswers = listOf( + "Answer 1", + "Answer 2", + "Answer 3", + "Answer 4" + ), + id = "" + )*/ ) } } \ No newline at end of file diff --git a/src/app/src/androidTest/java/fr/iut/alldev/allin/vo/bet/displayer/BetVOTestVisitor.kt b/src/app/src/androidTest/java/fr/iut/alldev/allin/vo/bet/displayer/BetVOTestVisitor.kt index b8abc92..3acb14d 100644 --- a/src/app/src/androidTest/java/fr/iut/alldev/allin/vo/bet/displayer/BetVOTestVisitor.kt +++ b/src/app/src/androidTest/java/fr/iut/alldev/allin/vo/bet/displayer/BetVOTestVisitor.kt @@ -4,25 +4,23 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import fr.iut.alldev.allin.data.model.bet.CustomBet -import fr.iut.alldev.allin.data.model.bet.MatchBet -import fr.iut.alldev.allin.data.model.bet.YesNoBet +import fr.iut.alldev.allin.data.model.bet.vo.BetDetail import fr.iut.alldev.allin.test.TestTags import fr.iut.alldev.allin.vo.bet.BetDisplayer class BetTestDisplayer : BetDisplayer { @Composable - override fun DisplayYesNoBet(bet: YesNoBet) { + override fun DisplayYesNoBet(betDetail: BetDetail) { Text("This is a YesNo Bet", Modifier.testTag(TestTags.YES_NO_BET.tag)) } @Composable - override fun DisplayMatchBet(bet: MatchBet) { + override fun DisplayMatchBet(betDetail: BetDetail) { Text("This is a Match Bet", Modifier.testTag(TestTags.MATCH_BET.tag)) } @Composable - override fun DisplayCustomBet(bet: CustomBet) { + override fun DisplayCustomBet(betDetail: BetDetail) { Text("This is a Custom Bet", Modifier.testTag(TestTags.CUSTOM_BET.tag)) } } \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt b/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt index 8181d68..bf23cf8 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ext/FieldExt.kt @@ -14,6 +14,9 @@ sealed class FieldErrorState( ) { data object NoError : FieldErrorState() + data object Mandatory : FieldErrorState(R.string.FieldError_Mandatory) + + data class TooShort(val fieldName: String, val minChar: Int) : FieldErrorState(R.string.FieldError_TooShort, fieldName, minChar) diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt index e7712ba..c72f746 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt @@ -5,17 +5,13 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.repository.BetRepository -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -23,14 +19,16 @@ class BetViewModel @Inject constructor( private val betRepository: BetRepository ) : ViewModel() { - private val _isRefreshing = MutableStateFlow(false) + private val _isRefreshing by lazy { MutableStateFlow(false) } val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + private val _bets: MutableStateFlow> by lazy { + MutableStateFlow(emptyList()) + } + val bets: StateFlow> by lazy { - flow { - kotlin.runCatching { emitAll(betRepository.getAllBets()) } - } + _bets.asStateFlow() .filterNotNull() .stateIn( viewModelScope, @@ -39,16 +37,22 @@ class BetViewModel @Inject constructor( ) } - private fun refreshData() { - Thread.sleep(1000) + init { + viewModelScope.launch { + refreshData() + } + } + + private suspend fun refreshData() { + runCatching { + _bets.emit(betRepository.getAllBets()) + } } fun refresh() { viewModelScope.launch { _isRefreshing.emit(true) - withContext(Dispatchers.IO) { - refreshData() - } + refreshData() _isRefreshing.emit(false) } } diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betConfirmation/BetConfirmationBottomSheet.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betConfirmation/BetConfirmationBottomSheet.kt new file mode 100644 index 0000000..de62423 --- /dev/null +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betConfirmation/BetConfirmationBottomSheet.kt @@ -0,0 +1,388 @@ +package fr.iut.alldev.allin.ui.betConfirmation + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.os.ConfigurationCompat +import fr.iut.alldev.allin.R +import fr.iut.alldev.allin.data.ext.formatToMediumDateNoYear +import fr.iut.alldev.allin.data.ext.formatToTime +import fr.iut.alldev.allin.data.model.bet.BetFinishedStatus +import fr.iut.alldev.allin.data.model.bet.BetStatus +import fr.iut.alldev.allin.data.model.bet.CustomBet +import fr.iut.alldev.allin.data.model.bet.MatchBet +import fr.iut.alldev.allin.data.model.bet.NO_VALUE +import fr.iut.alldev.allin.data.model.bet.YES_VALUE +import fr.iut.alldev.allin.data.model.bet.YesNoBet +import fr.iut.alldev.allin.data.model.bet.vo.BetAnswerDetail +import fr.iut.alldev.allin.data.model.bet.vo.BetDetail +import fr.iut.alldev.allin.ext.formatToSimple +import fr.iut.alldev.allin.theme.AllInTheme +import fr.iut.alldev.allin.ui.core.AllInBottomSheet +import fr.iut.alldev.allin.ui.core.AllInButton +import fr.iut.alldev.allin.ui.core.AllInCard +import fr.iut.alldev.allin.ui.core.AllInMarqueeBox +import fr.iut.alldev.allin.ui.core.bet.BetCard +import java.time.ZonedDateTime +import java.util.Locale + +@Composable +fun BetConfirmationBottomSheet( + state: SheetState, + sheetVisibility: Boolean, + betDetail: BetDetail, + onDismiss: () -> Unit, + onConfirm: (selectedAnswer: String) -> Unit +) { + AllInBottomSheet( + sheetVisibility = sheetVisibility, + onDismiss = onDismiss, + state = state, + dragHandle = null + ) { + BetConfirmationBottomSheetContent( + betDetail = betDetail, + onConfirm = { + onConfirm(it) + onDismiss() + }, + onClose = onDismiss + ) + } +} + +@Composable +fun BetConfirmationBottomSheetAnswer( + text: String, + odds: Float, + modifier: Modifier = Modifier, + color: Color = AllInTheme.colors.allInBlue, + isSelected: Boolean, + locale: Locale, + onClick: () -> Unit +) { + val backColor = if (isSelected) AllInTheme.colors.allInPurple else AllInTheme.colors.white + val contentColor = if (isSelected) AllInTheme.colors.white else null + + AllInCard( + backgroundColor = backColor, + onClick = onClick, + modifier = modifier + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 18.dp), + ) { + Text( + text = text.uppercase(), + color = contentColor ?: color, + style = AllInTheme.typography.h1, + fontSize = 40.sp, + modifier = Modifier.align(Alignment.Center) + ) + + AllInCard( + radius = 50.dp, + backgroundColor = contentColor ?: AllInTheme.colors.allInPurple, + modifier = Modifier.align(Alignment.CenterEnd) + ) { + Box(Modifier.padding(vertical = 4.dp, horizontal = 8.dp)) { + Text( + text = "x${odds.formatToSimple(locale)}", + color = backColor, + style = AllInTheme.typography.h2 + ) + } + } + } + } +} + +@Composable +fun ConfirmationAnswers( + betDetail: BetDetail, + selectedAnswer: String?, + onClick: (String) -> Unit +) { + val configuration = LocalConfiguration.current + val locale = remember { ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + when (betDetail.bet) { + is CustomBet -> items((betDetail.bet as CustomBet).possibleAnswers) { + betDetail.getAnswerOfResponse(it)?.let { + val opacity by animateFloatAsState(targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, label = "") + + + BetConfirmationBottomSheetAnswer( + text = it.response, + odds = it.odds, + locale = locale, + onClick = { onClick(it.response) }, + isSelected = selectedAnswer == it.response, + modifier = Modifier.alpha(opacity) + ) + } + } + + is MatchBet -> { + val bet = (betDetail.bet as MatchBet) + item { + betDetail.getAnswerOfResponse(bet.nameTeam1)?.let { + val opacity by animateFloatAsState(targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, label = "") + BetConfirmationBottomSheetAnswer( + text = it.response, + odds = it.odds, + locale = locale, + onClick = { onClick(it.response) }, + isSelected = selectedAnswer == it.response, + modifier = Modifier.alpha(opacity) + ) + } + } + item { + betDetail.getAnswerOfResponse(bet.nameTeam2)?.let { + val opacity by animateFloatAsState(targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, label = "") + + BetConfirmationBottomSheetAnswer( + text = it.response, + color = AllInTheme.colors.allInBarPink, + odds = it.odds, + locale = locale, + onClick = { onClick(it.response) }, + isSelected = selectedAnswer == it.response, + modifier = Modifier + .alpha(opacity) + ) + } + } + } + + is YesNoBet -> { + item { + betDetail.getAnswerOfResponse(YES_VALUE)?.let { + val opacity by animateFloatAsState(targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, label = "") + val scale by animateFloatAsState( + targetValue = if (selectedAnswer == null) 1f + else if (selectedAnswer != it.response) .95f else 1.05f, + label = "" + ) + + BetConfirmationBottomSheetAnswer( + text = it.response, + odds = it.odds, + locale = locale, + onClick = { onClick(it.response) }, + isSelected = selectedAnswer == it.response, + modifier = Modifier + .alpha(opacity) + .scale(scale) + ) + } + } + item { + betDetail.getAnswerOfResponse(NO_VALUE)?.let { + val opacity by animateFloatAsState(targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, label = "") + val scale by animateFloatAsState( + targetValue = if (selectedAnswer == null) 1f + else if (selectedAnswer != it.response) .95f else 1.05f, + label = "" + ) + + BetConfirmationBottomSheetAnswer( + text = it.response, + color = AllInTheme.colors.allInBarPink, + odds = it.odds, + locale = locale, + onClick = { onClick(it.response) }, + isSelected = selectedAnswer == it.response, + modifier = Modifier + .alpha(opacity) + .scale(scale) + ) + } + } + } + } + } +} + +@Composable +fun BetConfirmationBottomSheetContent( + betDetail: BetDetail, + onConfirm: (String) -> Unit, + onClose: () -> Unit +) { + var selectedAnswer by remember { mutableStateOf(null) } + + AllInMarqueeBox(backgroundColor = AllInTheme.colors.allInDarkGrey300) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + IconButton( + onClick = onClose, + modifier = Modifier + .size(24.dp) + .align(Alignment.TopStart) + ) { + Icon( + imageVector = Icons.Default.Close, + tint = AllInTheme.colors.white, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + Icon( + painter = painterResource(R.drawable.allin), + contentDescription = null, + tint = AllInTheme.colors.white, + modifier = Modifier + .size(40.dp) + .align(Alignment.TopCenter) + ) + + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.spacedBy(22.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BetCard( + title = betDetail.bet.phrase, + creator = betDetail.bet.creator, + category = betDetail.bet.theme, + date = betDetail.bet.endBetDate.formatToMediumDateNoYear(), + time = betDetail.bet.endBetDate.formatToTime(), + status = betDetail.bet.betStatus + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(AllInTheme.colors.allInMainGradient) + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.Finished), + color = AllInTheme.colors.white, + style = AllInTheme.typography.h1, + fontSize = 24.sp + ) + } + } + Text( + text = "Ce bet est arrivé à la date de fin. Vous devez à présent distribuer les gains en validant le pari gagnant.", + color = AllInTheme.colors.allInLightGrey200, + style = AllInTheme.typography.p2, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Veuillez choisir la réponse finale :", + fontSize = 20.sp, + color = AllInTheme.colors.white, + style = AllInTheme.typography.h1, + modifier = Modifier.fillMaxWidth() + ) + ConfirmationAnswers( + betDetail = betDetail, + selectedAnswer = selectedAnswer + ) { selectedAnswer = if (selectedAnswer == it) null else it } + } + if (selectedAnswer != null) { + AllInButton( + color = AllInTheme.colors.allInPurple, + text = stringResource(id = R.string.Validate), + textColor = AllInTheme.colors.white, + radius = 5.dp, + onClick = { selectedAnswer?.let(onConfirm) }, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + } + } +} + +@Preview +@Composable +private fun BetConfirmationBottomSheetContentPreview() { + AllInTheme { + BetConfirmationBottomSheetContent( + betDetail = BetDetail( + bet = YesNoBet( + id = "1", + theme = "Theme", + phrase = "Phrase", + endRegisterDate = ZonedDateTime.now(), + endBetDate = ZonedDateTime.now(), + isPublic = true, + betStatus = BetStatus.Finished(BetFinishedStatus.WON), + creator = "creator", + ), + answers = listOf( + BetAnswerDetail( + response = YES_VALUE, + totalStakes = 300, + totalParticipants = 2, + highestStake = 200, + odds = 1.0f + ), + BetAnswerDetail( + response = NO_VALUE, + totalStakes = 150, + totalParticipants = 1, + highestStake = 150, + odds = 2.0f + ) + ), + participations = emptyList(), + userParticipation = null + ), + onConfirm = { } + ) { + + } + } +} \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationScreen.kt index 2fd0126..7812f01 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationScreen.kt @@ -64,8 +64,6 @@ fun BetCreationScreen( val themeFieldName = stringResource(id = R.string.Theme) val phraseFieldName = stringResource(id = R.string.Bet_Phrase) - val registerDateFieldName = stringResource(id = R.string.End_registration_date) - val betDateFieldName = stringResource(id = R.string.End_bet_date) LaunchedEffect(key1 = betTypes) { selectionElements = betTypes.map { @@ -152,8 +150,6 @@ fun BetCreationScreen( viewModel.createBet( themeFieldName = themeFieldName, phraseFieldName = phraseFieldName, - registerDateFieldName = registerDateFieldName, - betDateFieldName = betDateFieldName, setLoading = setLoading, onError = { hasError = true }, onSuccess = { diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationViewModel.kt index 5e420ea..03a2f4b 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betCreation/BetCreationViewModel.kt @@ -17,9 +17,6 @@ import timber.log.Timber import java.time.ZonedDateTime import javax.inject.Inject -const val THEME_MIN_SIZE = 3 -const val PHRASE_MIN_SIZE = 5 - @HiltViewModel class BetCreationViewModel @Inject constructor( @AllInCurrentUser val currentUser: User, @@ -27,7 +24,7 @@ class BetCreationViewModel @Inject constructor( private val keystoreManager: AllInKeystoreManager ) : ViewModel() { - var hasError = mutableStateOf(false) + private var hasError = mutableStateOf(false) var theme = mutableStateOf("") var phrase = mutableStateOf("") val registerDate = mutableStateOf(ZonedDateTime.now()) @@ -49,20 +46,16 @@ class BetCreationViewModel @Inject constructor( } private fun verifyField( - themeFieldName: String, - phraseFieldName: String, registerDateFieldName: String, betDateFieldName: String, ) { - if (theme.value.length < THEME_MIN_SIZE) { - themeError.value = - FieldErrorState.TooShort(themeFieldName.lowercase(), THEME_MIN_SIZE) + if (theme.value.isBlank()) { + themeError.value = FieldErrorState.Mandatory hasError.value = true } - if (phrase.value.length < PHRASE_MIN_SIZE) { - phraseError.value = - FieldErrorState.TooShort(phraseFieldName.lowercase(), PHRASE_MIN_SIZE) + if (phrase.value.isBlank()) { + phraseError.value = FieldErrorState.Mandatory hasError.value = true } @@ -90,8 +83,6 @@ class BetCreationViewModel @Inject constructor( fun createBet( themeFieldName: String, phraseFieldName: String, - registerDateFieldName: String, - betDateFieldName: String, onError: () -> Unit, setLoading: (Boolean) -> Unit, onSuccess: () -> Unit @@ -103,8 +94,6 @@ class BetCreationViewModel @Inject constructor( verifyField( themeFieldName, phraseFieldName, - registerDateFieldName, - betDateFieldName, ) if (!hasError.value) { diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betResult/BetResultBottomSheet.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betResult/BetResultBottomSheet.kt index 7fd4eeb..b7d30b3 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/betResult/BetResultBottomSheet.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betResult/BetResultBottomSheet.kt @@ -1,13 +1,9 @@ package fr.iut.alldev.allin.ui.betResult -import androidx.compose.foundation.MarqueeSpacing -import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons @@ -18,8 +14,6 @@ import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.scale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -34,6 +28,7 @@ import fr.iut.alldev.allin.ui.betResult.components.BetResultBottomSheetBetCard import fr.iut.alldev.allin.ui.betResult.components.BetResultBottomSheetContentCoinAmount import fr.iut.alldev.allin.ui.betResult.components.BetResultBottomSheetContentCongratulations import fr.iut.alldev.allin.ui.core.AllInBottomSheet +import fr.iut.alldev.allin.ui.core.AllInMarqueeBox import java.time.ZonedDateTime @Composable @@ -76,22 +71,7 @@ fun BetResultBottomSheetContent( odds: Float, onClose: () -> Unit ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(AllInTheme.colors.allInMainGradientReverse), - ) { - Icon( - painter = painterResource(id = R.drawable.allin_marquee), - contentDescription = null, - modifier = Modifier - .fillMaxSize() - .rotate(11f) - .scale(1.2f) - .offset(x = (-24).dp) - .basicMarquee(spacing = MarqueeSpacing(0.dp)), - tint = AllInTheme.colors.white.copy(alpha = .05f) - ) + AllInMarqueeBox(backgroundBrush = AllInTheme.colors.allInMainGradientReverse) { Box( modifier = Modifier .fillMaxSize() @@ -146,7 +126,6 @@ fun BetResultBottomSheetContent( } @Preview -@Preview(widthDp = 800, heightDp = 1280) @Composable private fun BetResultBottomSheetContentPreview() { AllInTheme { diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/components/YesNoDetailsLine.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/components/YesNoDetailsLine.kt index 784b368..78f0f91 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/components/YesNoDetailsLine.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/components/YesNoDetailsLine.kt @@ -7,42 +7,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.tooling.preview.Preview import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.ui.core.AllInTextIcon import fr.iut.alldev.allin.ui.core.IconPosition -@Composable -fun YesNoDetailsLine( - icon: ImageVector, - yesText: String, - noText: String, -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - AllInTextIcon( - text = yesText, - color = AllInTheme.colors.allInBlue, - icon = rememberVectorPainter(image = icon), - position = IconPosition.LEADING, - size = 10, - iconSize = 15 - ) - AllInTextIcon( - text = noText, - color = AllInTheme.colors.allInBarPink, - icon = rememberVectorPainter(image = icon), - position = IconPosition.TRAILING, - size = 10, - iconSize = 15 - ) - } -} - @Composable fun YesNoDetailsLine( icon: Painter, @@ -58,7 +27,7 @@ fun YesNoDetailsLine( color = AllInTheme.colors.allInBlue, icon = icon, position = IconPosition.LEADING, - size = 10, + size = 15, iconSize = 15 ) AllInTextIcon( @@ -66,7 +35,7 @@ fun YesNoDetailsLine( color = AllInTheme.colors.allInBarPink, icon = icon, position = IconPosition.TRAILING, - size = 10, + size = 15, iconSize = 15 ) } diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/vo/BetStatusBottomSheetBetDisplayer.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/vo/BetStatusBottomSheetBetDisplayer.kt index e316c59..a190ee8 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/vo/BetStatusBottomSheetBetDisplayer.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/betStatus/vo/BetStatusBottomSheetBetDisplayer.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -131,17 +132,17 @@ class BetStatusBottomSheetBetDisplayer( noText = response2Answer?.totalStakes?.toString() ?: "0" ) YesNoDetailsLine( - icon = Icons.Filled.People, + icon = rememberVectorPainter(image = Icons.Filled.People), yesText = response1Answer?.totalParticipants?.toString() ?: "0", noText = response2Answer?.totalParticipants?.toString() ?: "0" ) YesNoDetailsLine( - icon = Icons.Filled.WorkspacePremium, + icon = rememberVectorPainter(image = Icons.Filled.WorkspacePremium), yesText = response1Answer?.highestStake?.toString() ?: "0", noText = response2Answer?.highestStake?.toString() ?: "0" ) YesNoDetailsLine( - icon = Icons.Filled.EmojiEvents, + icon = rememberVectorPainter(image = Icons.Filled.EmojiEvents), yesText = "x${response1Answer?.odds?.formatToSimple(locale) ?: "1.00"}", noText = "x${response2Answer?.odds?.formatToSimple(locale) ?: "1.00"}" ) diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInMarqueeBox.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInMarqueeBox.kt new file mode 100644 index 0000000..22c420e --- /dev/null +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInMarqueeBox.kt @@ -0,0 +1,65 @@ +package fr.iut.alldev.allin.ui.core + +import androidx.compose.foundation.MarqueeSpacing +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import fr.iut.alldev.allin.R +import fr.iut.alldev.allin.theme.AllInTheme + +@Composable +fun AllInMarqueeBox( + backgroundColor: Color = AllInTheme.themeColors.mainSurface, + backgroundBrush: Brush? = null, + content: @Composable BoxScope.() -> Unit + +) { + Box( + modifier = Modifier + .fillMaxSize().let { itModifier -> + backgroundBrush?.let { + itModifier.background(it) + } ?: itModifier.background(backgroundColor) + } + ) { + Icon( + painter = painterResource(id = R.drawable.allin_marquee), + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f, true) + .scale(1.2f) + .rotate(11f) + .basicMarquee(spacing = MarqueeSpacing(0.dp)), + tint = AllInTheme.colors.white.copy(alpha = .05f) + ) + content() + } +} + +@Preview +@Preview(widthDp = 800, heightDp = 1280) +@Composable +private fun AllInMarqueeBoxPreview() { + AllInTheme { + AllInMarqueeBox( + backgroundBrush = AllInTheme.colors.allInMainGradient, + ) { + Text("CONTENT") + } + } +} \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainScreen.kt index fdd9642..2e8f57c 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainScreen.kt @@ -11,6 +11,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import fr.iut.alldev.allin.theme.AllInTheme +import fr.iut.alldev.allin.ui.betConfirmation.BetConfirmationBottomSheet import fr.iut.alldev.allin.ui.betResult.BetResultBottomSheet import fr.iut.alldev.allin.ui.betStatus.BetStatusBottomSheet import fr.iut.alldev.allin.ui.betStatus.vo.BetStatusBottomSheetBetDisplayer @@ -68,11 +69,10 @@ fun MainScreen( var loading by remember { mainViewModel.loading } val currentUser = remember { mainViewModel.currentUserState } val selectedBet by remember { mainViewModel.selectedBet } - val (wonBet, setWonBet) = remember { mainViewModel.wonBet } + val wonBet by remember { mainViewModel.wonBet } + val toConfirm by remember { mainViewModel.toConfirmBet } val (statusVisibility, sheetBackVisibility, setStatusVisibility) = rememberBetStatusVisibilities() - val (participateSheetVisibility, setParticipateSheetVisibility) = remember { - mutableStateOf(false) - } + val (participateSheetVisibility, setParticipateSheetVisibility) = remember { mutableStateOf(false) } val (displayResult, setDisplayResult) = remember { mutableStateOf(true) } @@ -168,7 +168,7 @@ fun MainScreen( state = resultBottomSheetState, sheetVisibility = displayResult, onDismiss = { setDisplayResult(false) }, - bet = wonBet, + bet = it, username = currentUser.user.username, coinAmount = 1630, stake = 1630, @@ -177,6 +177,15 @@ fun MainScreen( ) } + toConfirm?.let { + BetConfirmationBottomSheet( + state = resultBottomSheetState, + sheetVisibility = displayResult, + betDetail = it, + onDismiss = { setDisplayResult(false) } + ) { /*TODO*/ } + } + BetStatusBottomSheet( state = statusBottomSheetState, sheetVisibility = statusVisibility.value, diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainViewModel.kt index a666bef..6f96a21 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/main/MainViewModel.kt @@ -8,7 +8,13 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.model.bet.Bet +import fr.iut.alldev.allin.data.model.bet.BetFinishedStatus +import fr.iut.alldev.allin.data.model.bet.BetStatus +import fr.iut.alldev.allin.data.model.bet.NO_VALUE import fr.iut.alldev.allin.data.model.bet.Participation +import fr.iut.alldev.allin.data.model.bet.YES_VALUE +import fr.iut.alldev.allin.data.model.bet.YesNoBet +import fr.iut.alldev.allin.data.model.bet.vo.BetAnswerDetail import fr.iut.alldev.allin.data.model.bet.vo.BetDetail import fr.iut.alldev.allin.data.repository.BetRepository import fr.iut.alldev.allin.di.AllInCurrentUser @@ -17,6 +23,7 @@ import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.time.ZonedDateTime import javax.inject.Inject class UserState(val user: User) { @@ -48,6 +55,39 @@ class MainViewModel @Inject constructor( )*/ ) + val toConfirmBet = mutableStateOf( + BetDetail( + bet = YesNoBet( + id = "1", + theme = "Theme", + phrase = "Phrase", + endRegisterDate = ZonedDateTime.now(), + endBetDate = ZonedDateTime.now(), + isPublic = true, + betStatus = BetStatus.Finished(BetFinishedStatus.WON), + creator = "creator", + ), + answers = listOf( + BetAnswerDetail( + response = YES_VALUE, + totalStakes = 300, + totalParticipants = 2, + highestStake = 200, + odds = 1.0f + ), + BetAnswerDetail( + response = NO_VALUE, + totalStakes = 150, + totalParticipants = 1, + highestStake = 150, + odds = 2.0f + ) + ), + participations = emptyList(), + userParticipation = null + ) + ) + val snackbarContent: MutableState by lazy { mutableStateOf(null) } fun putSnackbarContent(content: SnackbarContent) { snackbarContent.value = content diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/preview/BetDetailPreviewProvider.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/preview/BetDetailPreviewProvider.kt index 154f04c..e3d9d95 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/preview/BetDetailPreviewProvider.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/preview/BetDetailPreviewProvider.kt @@ -25,7 +25,7 @@ class BetDetailPreviewProvider : PreviewParameterProvider { totalParticipants = 1, highestStake = 150, odds = 2.0f - ), + ) ), participations = listOf( Participation( diff --git a/src/app/src/main/res/values-fr/strings.xml b/src/app/src/main/res/values-fr/strings.xml index 0f1e299..f24b3b4 100644 --- a/src/app/src/main/res/values-fr/strings.xml +++ b/src/app/src/main/res/values-fr/strings.xml @@ -14,6 +14,7 @@ Valider Annuler OK + Ce champ est obligatoire. Le %s doit contenir au moins %d caractères. Le %s a un mauvais format : %s. Les champs ne sont pas identiques. diff --git a/src/app/src/main/res/values/strings.xml b/src/app/src/main/res/values/strings.xml index 328e981..fb8050c 100644 --- a/src/app/src/main/res/values/strings.xml +++ b/src/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ Validate Cancel OK + This field is mandatory. The %s must contain at least %d characters. The %s has bad format : %s. The fields are not identical. diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/BetRepository.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/BetRepository.kt index ed4bcd8..f6f345b 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/BetRepository.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/BetRepository.kt @@ -11,5 +11,5 @@ abstract class BetRepository { abstract suspend fun getCurrentBets(): Flow> abstract suspend fun getBet(id: String, token: String): BetDetail abstract suspend fun participateToBet(participation: Participation, token: String) - abstract suspend fun getAllBets(): Flow> + abstract suspend fun getAllBets(): List } \ No newline at end of file diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/BetRepositoryImpl.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/BetRepositoryImpl.kt index 7748092..0df5d67 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/BetRepositoryImpl.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/BetRepositoryImpl.kt @@ -110,10 +110,8 @@ class BetRepositoryImpl @Inject constructor( api.participateToBet(token = token.formatBearerToken(), body = participation.toRequestParticipation()) } - override suspend fun getAllBets(): Flow> { - return flowOf( - api.getAllBets().map { it.toBet() } - ) + override suspend fun getAllBets(): List { + return api.getAllBets().map { it.toBet() } } } \ No newline at end of file