Bet creation
continuous-integration/drone/push Build is passing Details

pull/3/head
Arthur VALIN 1 year ago
parent b575f6e157
commit 269dfdd433

@ -31,7 +31,6 @@ import fr.iut.alldev.allin.ui.core.AllInTimePicker
import fr.iut.alldev.allin.ui.core.RainbowButton
import fr.iut.alldev.allin.ui.core.SectionElement
import fr.iut.alldev.allin.ui.core.SelectionElement
import fr.iut.alldev.allin.ui.main.MainViewModel
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
@ -39,7 +38,8 @@ import java.time.ZonedDateTime
@Composable
fun BetCreationScreen(
viewModel: BetCreationViewModel = hiltViewModel(),
mainViewModel: MainViewModel,
setLoading: (Boolean) -> Unit,
onCreation: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val betTypes = remember { BetType.values().toList() }
@ -154,8 +154,11 @@ fun BetCreationScreen(
phraseFieldName = phraseFieldName,
registerDateFieldName = registerDateFieldName,
betDateFieldName = betDateFieldName,
setLoading = { mainViewModel.loading.value = it },
onError = { hasError = true }
setLoading = setLoading,
onError = { hasError = true },
onSuccess = {
onCreation()
}
)
}
)

@ -11,6 +11,7 @@ import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.data.repository.BetRepository
import fr.iut.alldev.allin.di.AllInCurrentUser
import fr.iut.alldev.allin.ext.FieldErrorState
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.ZonedDateTime
@ -23,6 +24,7 @@ const val PHRASE_MIN_SIZE = 5
class BetCreationViewModel @Inject constructor(
@AllInCurrentUser val currentUser: User,
private val betRepository: BetRepository,
private val keystoreManager: AllInKeystoreManager
) : ViewModel() {
var hasError = mutableStateOf(false)
@ -92,6 +94,7 @@ class BetCreationViewModel @Inject constructor(
betDateFieldName: String,
onError: () -> Unit,
setLoading: (Boolean) -> Unit,
onSuccess: () -> Unit
) {
viewModelScope.launch {
setLoading(true)
@ -118,7 +121,8 @@ class BetCreationViewModel @Inject constructor(
possibleAnswers = setOf(),
creator = currentUser.username
)
betRepository.createBet(bet)
betRepository.createBet(bet, keystoreManager.getToken() ?: "")
onSuccess()
} catch (e: AllInAPIException) {
Timber.e(e)
onError()

@ -0,0 +1,148 @@
package fr.iut.alldev.allin.ui.core.snackbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.theme.AllInTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AllInSnackbar(
snackbarState: SnackbarHostState
) {
SnackbarHost(
hostState = snackbarState
) { snackbarData ->
val dismissState = rememberSwipeToDismissState(
confirmValueChange = { value ->
if (value != SwipeToDismissValue.Settled) {
snackbarState.currentSnackbarData?.dismiss()
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {},
modifier = Modifier.padding(8.dp)
) {
val snackbarType = remember {
if (snackbarData.visuals is AllInSnackbarVisualsImpl) {
(snackbarData.visuals as AllInSnackbarVisualsImpl).type
} else {
SnackbarType.STANDARD
}
}
AllInSnackbarContent(
backgroundColor = snackbarType.getBackgroundColor(),
contentColor = AllInTheme.colors.white,
text = snackbarData.visuals.message,
icon = snackbarType.getIcon(),
dismiss = { snackbarState.currentSnackbarData?.dismiss() }
)
}
}
}
@Composable
fun AllInSnackbarContent(
backgroundColor: Color,
contentColor: Color,
text: String,
icon: ImageVector,
dismiss: () -> Unit
) {
Surface(
shape = RoundedCornerShape(16.dp),
shadowElevation = 4.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(8.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = contentColor,
modifier = Modifier.size(24.dp)
)
Text(
text = text,
color = contentColor,
style = AllInTheme.typography.r,
overflow = TextOverflow.Ellipsis,
maxLines = 5,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = dismiss,
modifier = Modifier
.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = contentColor,
modifier = Modifier.size(24.dp)
)
}
}
}
}
private class SnackbarTypePreviewProvider : PreviewParameterProvider<SnackbarType> {
override val values = SnackbarType.entries.asSequence()
}
@Preview
@Composable
private fun AllInSnackbarContentPreview(
@PreviewParameter(SnackbarTypePreviewProvider::class) snackbarType: SnackbarType
) {
AllInTheme {
AllInSnackbarContent(
backgroundColor = snackbarType.getBackgroundColor(),
contentColor = AllInTheme.colors.white,
text = "Lorem Ipsum",
icon = snackbarType.getIcon(),
dismiss = {}
)
}
}

@ -0,0 +1,36 @@
package fr.iut.alldev.allin.ui.core.snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarVisuals
class AllInSnackbarVisualsImpl(
override val message: String,
override val actionLabel: String? = null,
override val withDismissAction: Boolean,
override val duration: SnackbarDuration,
val type: SnackbarType
) : SnackbarVisuals {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as AllInSnackbarVisualsImpl
if (message != other.message) return false
if (actionLabel != other.actionLabel) return false
if (withDismissAction != other.withDismissAction) return false
if (duration != other.duration) return false
if (type != other.type) return false
return true
}
override fun hashCode(): Int {
var result = message.hashCode()
result = 31 * result + actionLabel.hashCode()
result = 31 * result + withDismissAction.hashCode()
result = 31 * result + duration.hashCode()
result = 31 * result + type.hashCode()
return result
}
}

@ -0,0 +1,35 @@
package fr.iut.alldev.allin.ui.core.snackbar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType.ERROR
import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType.STANDARD
import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType.SUCCESS
enum class SnackbarType {
STANDARD,
SUCCESS,
ERROR
}
@Composable
fun SnackbarType.getBackgroundColor(): Color =
when (this) {
STANDARD -> AllInTheme.colors.allInDark
SUCCESS -> AllInTheme.colors.allInPurple
ERROR -> AllInTheme.colors.allInBetWaiting
}
@Composable
fun SnackbarType.getIcon(): ImageVector =
when (this) {
STANDARD -> Icons.Default.Info
ERROR -> Icons.Default.Error
SUCCESS -> Icons.Default.CheckCircle
}

@ -14,6 +14,7 @@ import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.betStatus.BetStatusBottomSheet
import fr.iut.alldev.allin.ui.betStatus.visitor.BetStatusBottomSheetDisplayBetVisitor
import fr.iut.alldev.allin.ui.core.AllInLoading
import fr.iut.alldev.allin.ui.core.snackbar.AllInSnackbarVisualsImpl
import fr.iut.alldev.allin.ui.main.components.AllInScaffold
import fr.iut.alldev.allin.ui.navigation.AllInDrawerNavHost
import fr.iut.alldev.allin.ui.navigation.Routes
@ -62,15 +63,12 @@ fun MainScreen(
mainViewModel: MainViewModel = hiltViewModel(),
navigateToWelcomeScreen: () -> Unit
) {
val loading by remember { mainViewModel.loading }
val scope = rememberCoroutineScope()
val currentUser = remember {
mainViewModel.currentUserState
}
val (selectedBet, setSelectedBet) = remember {
mainViewModel.selectedBet
}
var loading by remember { mainViewModel.loading }
val currentUser = remember { mainViewModel.currentUserState }
val (selectedBet, setSelectedBet) = remember { mainViewModel.selectedBet }
val (statusVisibility, sheetBackVisibility, setStatusVisibility) = rememberBetStatusVisibilities()
val betStatusDisplayVisitor = remember {
BetStatusBottomSheetDisplayBetVisitor(
@ -81,10 +79,26 @@ fun MainScreen(
)
}
val scope = rememberCoroutineScope()
val (statusVisibility, sheetBackVisibility, setStatusVisibility)
= rememberBetStatusVisibilities()
val snackbarHostState = remember { SnackbarHostState() }
var snackbarContent by remember { mainViewModel.snackbarContent }
LaunchedEffect(snackbarContent) {
snackbarContent?.let {
scope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
snackbarHostState.showSnackbar(
AllInSnackbarVisualsImpl(
message = it.text,
withDismissAction = false,
duration = SnackbarDuration.Short,
type = it.type
)
)
snackbarHostState.currentSnackbarData
snackbarContent = null
}
}
}
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
@ -115,7 +129,8 @@ fun MainScreen(
AllInScaffold(
onMenuClicked = { scope.launch { drawerState.open() } },
coinAmount = currentUser.userCoins.value,
drawerState = drawerState
drawerState = drawerState,
snackbarHostState = snackbarHostState
) {
LaunchedEffect(key1 = it) {
betStatusDisplayVisitor.paddingValues.value = it
@ -129,12 +144,13 @@ fun MainScreen(
) {
AllInDrawerNavHost(
navController = navController,
mainViewModel = mainViewModel,
selectBet = { bet, participate ->
setSelectedBet(bet)
betStatusDisplayVisitor.participateBottomSheetVisibility.value = participate
setStatusVisibility(true)
}
},
setLoading = { loading = it },
putSnackbarContent = { mainViewModel.putSnackbarContent(it) }
)
}
}

@ -1,5 +1,6 @@
package fr.iut.alldev.allin.ui.main
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@ -9,6 +10,7 @@ import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.di.AllInCurrentUser
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -29,6 +31,12 @@ class MainViewModel @Inject constructor(
val currentUserState = UserState(currentUser)
val selectedBet = mutableStateOf<Bet?>(null)
val snackbarContent: MutableState<SnackbarContent?> by lazy { mutableStateOf(null) }
fun putSnackbarContent(content: SnackbarContent) {
snackbarContent.value = content
}
fun deleteToken() {
viewModelScope.launch {
keystoreManager.deleteToken()
@ -46,4 +54,8 @@ class MainViewModel @Inject constructor(
}
}
class SnackbarContent(
val text: String,
val type: SnackbarType = SnackbarType.STANDARD
)
}

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -14,6 +15,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.ui.core.snackbar.AllInSnackbar
import fr.iut.alldev.allin.ui.core.topbar.AllInTopBar
import kotlin.math.abs
@ -22,6 +24,7 @@ fun AllInScaffold(
onMenuClicked: () -> Unit,
coinAmount: Int,
drawerState: DrawerState,
snackbarHostState: SnackbarHostState,
content: @Composable (PaddingValues) -> Unit,
) {
@ -42,6 +45,9 @@ fun AllInScaffold(
Scaffold(
modifier = Modifier.offset(x = contentOffset),
snackbarHost = {
AllInSnackbar(snackbarState = snackbarHostState)
},
topBar = {
AllInTopBar(
onMenuClicked = onMenuClicked,

@ -11,6 +11,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
@ -19,11 +20,13 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.bet.BetScreen
import fr.iut.alldev.allin.ui.betCreation.BetCreationScreen
import fr.iut.alldev.allin.ui.betHistory.BetHistoryScreen
import fr.iut.alldev.allin.ui.core.snackbar.SnackbarType
import fr.iut.alldev.allin.ui.login.LoginScreen
import fr.iut.alldev.allin.ui.main.MainScreen
import fr.iut.alldev.allin.ui.main.MainViewModel
@ -93,9 +96,10 @@ fun AllInNavHost(
internal fun AllInDrawerNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
mainViewModel: MainViewModel,
selectBet: (Bet, Boolean) -> Unit,
startDestination: String = Routes.PUBLIC_BETS,
setLoading: (Boolean) -> Unit,
putSnackbarContent: (MainViewModel.SnackbarContent) -> Unit
) {
NavHost(
navController = navController,
@ -110,8 +114,18 @@ internal fun AllInDrawerNavHost(
)
}
composable(route = Routes.BET_CREATION) {
val creationSuccessMessage = stringResource(id = R.string.bet_creation_success_message)
BetCreationScreen(
mainViewModel = mainViewModel
setLoading = setLoading,
onCreation = {
putSnackbarContent(
MainViewModel.SnackbarContent(
text = creationSuccessMessage,
type = SnackbarType.SUCCESS
)
)
navController.popUpTo(Routes.PUBLIC_BETS, Routes.BET_CREATION)
}
)
}

@ -82,6 +82,7 @@
<string name="yes_no_bottom_text_2">Aucune autre réponse ne sera acceptée.</string>
<string name="custom_answers">Réponses personnalisées</string>
<string name="bet_creation_error">Erreur lors de la création du bet, veuillez rééssayer.</string>
<string name="bet_creation_success_message">Bet créé !</string>
<!--Bet Page-->
<string name="Popular">Populaire</string>

@ -85,6 +85,7 @@
<string name="sport_match">Sport match</string>
<string name="custom_answers">Custom answers</string>
<string name="bet_creation_error">Error while creating the bet. Please try again.</string>
<string name="bet_creation_success_message">Bet created !</string>
<!--Bet Page-->
<string name="Popular">Popular</string>

@ -1,8 +1,8 @@
package fr.iut.alldev.allin.data.api
import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestBet
import fr.iut.alldev.allin.data.api.model.RequestUser
import fr.iut.alldev.allin.data.api.model.ResponseBet
import fr.iut.alldev.allin.data.api.model.ResponseUser
import retrofit2.http.Body
import retrofit2.http.GET
@ -20,5 +20,5 @@ interface AllInApi {
suspend fun login(@Header("Authorization") token: String): ResponseUser
@POST("bets/add")
suspend fun createBet(@Body body: ResponseBet)
suspend fun createBet(@Body body: RequestBet)
}

@ -5,7 +5,7 @@ import fr.iut.alldev.allin.data.model.bet.Bet
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.YesNoBet
import fr.iut.alldev.allin.data.serialization.ZonedDateTimeSerializer
import fr.iut.alldev.allin.data.serialization.SimpleDateSerializer
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
@ -15,11 +15,11 @@ data class ResponseBet(
val id: Int?,
val theme: String,
val sentenceBet: String,
@Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime,
@Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime,
@Serializable(SimpleDateSerializer::class) val endRegistration: ZonedDateTime,
@Serializable(SimpleDateSerializer::class) var endBet: ZonedDateTime,
var isPrivate: Boolean,
var response: List<String>,
val createdBy: String,
val createdBy: String
) {
fun toBet(): Bet {
if (response.toSet() == setOf("Yes", "No")) {
@ -46,3 +46,15 @@ data class ResponseBet(
}
}
}
@Keep
@Serializable
data class RequestBet(
val theme: String,
val sentenceBet: String,
@Serializable(SimpleDateSerializer::class) val endRegistration: ZonedDateTime,
@Serializable(SimpleDateSerializer::class) var endBet: ZonedDateTime,
var isPrivate: Boolean,
var response: List<String>,
val createdBy: String
)

@ -1,6 +1,6 @@
package fr.iut.alldev.allin.data.model.bet
import fr.iut.alldev.allin.data.api.model.ResponseBet
import fr.iut.alldev.allin.data.api.model.RequestBet
import java.time.ZonedDateTime
abstract class Bet(
@ -14,9 +14,8 @@ abstract class Bet(
open val betStatus: BetStatus,
) {
abstract fun getResponses(): List<String>
fun toResponseBet(): ResponseBet {
return ResponseBet(
id = id,
fun toRequestBet(): RequestBet {
return RequestBet(
theme = theme,
sentenceBet = phrase,
endRegistration = endRegisterDate,

@ -4,7 +4,7 @@ import fr.iut.alldev.allin.data.model.bet.Bet
import kotlinx.coroutines.flow.Flow
abstract class BetRepository {
abstract suspend fun createBet(bet: Bet)
abstract suspend fun createBet(bet: Bet, token: String)
abstract suspend fun getHistory(): Flow<List<Bet>>
abstract suspend fun getCurrentBets(): Flow<List<Bet>>
}

@ -14,8 +14,10 @@ import javax.inject.Inject
class BetRepositoryImpl @Inject constructor(
private val api: AllInApi
) : BetRepository() {
override suspend fun createBet(bet: Bet) {
api.createBet(bet.toResponseBet())
override suspend fun createBet(bet: Bet, token: String) {
api.createBet(
bet.toRequestBet().copy(createdBy = token)
)
}
override suspend fun getHistory(): Flow<List<Bet>> {

@ -7,8 +7,10 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
override val descriptor: SerialDescriptor =
@ -22,4 +24,21 @@ object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
val epoch = decoder.decodeLong()
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epoch), ZoneId.systemDefault())
}
}
}
class SimpleDateSerializer : KSerializer<ZonedDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
override fun deserialize(decoder: Decoder): ZonedDateTime {
val date = LocalDate.parse(decoder.decodeString(), formatter)
return date.atStartOfDay(ZoneId.systemDefault())
}
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
val dateString = formatter.format(value)
encoder.encodeString(dateString)
}
}

Loading…
Cancel
Save