You've already forked RekomenciMobile
fix: fix phone field on profile screen, bottom bar beautify; feat: show buttons only after change, on profile edit
This commit is contained in:
+6
-1
@@ -25,6 +25,7 @@ data class ValidationResult(
|
|||||||
@Single
|
@Single
|
||||||
class ValidateAuthFieldsUseCase {
|
class ValidateAuthFieldsUseCase {
|
||||||
fun validateProfile(
|
fun validateProfile(
|
||||||
|
chosenPattern: PhoneNumberPattern?,
|
||||||
firstName: String,
|
firstName: String,
|
||||||
lastName: String,
|
lastName: String,
|
||||||
email: String,
|
email: String,
|
||||||
@@ -34,7 +35,11 @@ class ValidateAuthFieldsUseCase {
|
|||||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||||
if (!isPhoneValid(phone)) put(AuthField.Phone, "Некорректный номер телефона")
|
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||||
|
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||||
|
AuthField.Phone,
|
||||||
|
"Некорректный номер телефона"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return ValidationResult(errors)
|
return ValidationResult(errors)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -4,6 +4,8 @@ import android.util.Log
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -59,7 +61,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
|||||||
}
|
}
|
||||||
target?.let { (it - center).toDp() }
|
target?.let { (it - center).toDp() }
|
||||||
}
|
}
|
||||||
AnimatedVisibility(indicatorOffset != null) {
|
|
||||||
indicatorOffset?.let {
|
indicatorOffset?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -71,7 +72,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.components.standart
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TPhoneField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
currentPattern: UIPhoneNumberPattern?,
|
||||||
|
currentPhone: String,
|
||||||
|
onPhoneChange: (String) -> Unit,
|
||||||
|
error: String?,
|
||||||
|
onOpenCountryList: () -> Unit,
|
||||||
|
) {
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
Row(
|
||||||
|
modifier = modifier.height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
Paddings.medium
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
FieldWrapper(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(IntrinsicSize.Min)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.offset(y = 5.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.background(colorScheme.primary, Shapes.smallRoundedBox)
|
||||||
|
.clip(Shapes.smallRoundedBox),
|
||||||
|
value = currentPattern?.prefix ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = colorScheme.onPrimary
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
onOpenCountryList()
|
||||||
|
}
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_arr_dropdown),
|
||||||
|
tint = colorScheme.onPrimary,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TTTextField(
|
||||||
|
value = currentPhone,
|
||||||
|
onValueChange = onPhoneChange,
|
||||||
|
label = "Ваш телефон",
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Phone
|
||||||
|
),
|
||||||
|
visualTransformation = currentPattern?.let {
|
||||||
|
PhoneVisualTransformation(
|
||||||
|
it.pattern,
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
} ?: VisualTransformation.None,
|
||||||
|
error = error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TPhoneCountryList(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isSheetOpen: MutableState<Boolean>,
|
||||||
|
sheetState: SheetState,
|
||||||
|
patternList: List<UIPhoneNumberPattern>,
|
||||||
|
setPattern: (UIPhoneNumberPattern) -> Unit
|
||||||
|
) {
|
||||||
|
if (isSheetOpen.value) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier,
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = {
|
||||||
|
isSheetOpen.value = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(patternList) { pattern ->
|
||||||
|
Text(
|
||||||
|
text = pattern.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
.clickable {
|
||||||
|
setPattern(pattern)
|
||||||
|
isSheetOpen.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-87
@@ -44,7 +44,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -56,6 +55,8 @@ import com.prodhack.moscow2025.R
|
|||||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
@@ -165,70 +166,16 @@ fun ErrorCollectorScope.FillProfileScreen(
|
|||||||
error = formState.errors[AuthField.LastName],
|
error = formState.errors[AuthField.LastName],
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
TPhoneField(
|
||||||
Row(
|
currentPattern = viewModel.currentPattern.value,
|
||||||
modifier = Modifier.height(IntrinsicSize.Min),
|
currentPhone = formState.phone,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onPhoneChange = viewModel::onPhoneChange,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
error = formState.errors[AuthField.Phone],
|
||||||
Paddings.medium
|
onOpenCountryList =
|
||||||
)
|
{
|
||||||
) {
|
|
||||||
FieldWrapper(modifier = Modifier
|
|
||||||
.width(IntrinsicSize.Min)
|
|
||||||
.fillMaxHeight()) {
|
|
||||||
BasicTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.offset(y = 5.dp)
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.background(colorScheme.primary, Shapes.smallRoundedBox)
|
|
||||||
.clip(Shapes.smallRoundedBox),
|
|
||||||
value = viewModel.chosenPattern.value?.prefix ?: "",
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = colorScheme.onPrimary
|
|
||||||
),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
isSheetOpen.value = true
|
isSheetOpen.value = true
|
||||||
}
|
}
|
||||||
.padding(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(15.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_arr_dropdown),
|
|
||||||
tint = colorScheme.onPrimary,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TTTextField(
|
|
||||||
value = formState.phone,
|
|
||||||
onValueChange = viewModel::onPhoneChange,
|
|
||||||
label = "Ваш телефон",
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Phone
|
|
||||||
),
|
|
||||||
visualTransformation = viewModel.chosenPattern.value?.pattern?.let {
|
|
||||||
PhoneVisualTransformation(
|
|
||||||
it,
|
|
||||||
'0'
|
|
||||||
)
|
|
||||||
} ?: VisualTransformation.None,
|
|
||||||
error = formState.errors[AuthField.Phone]
|
|
||||||
)
|
|
||||||
Log.d("Test", formState.errors[AuthField.Phone].toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
BigButton(
|
BigButton(
|
||||||
@@ -241,32 +188,12 @@ fun ErrorCollectorScope.FillProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSheetOpen.value) {
|
TPhoneCountryList(
|
||||||
ModalBottomSheet(
|
isSheetOpen = isSheetOpen,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = {
|
patternList = viewModel.phoneNumberPatterns,
|
||||||
isSheetOpen.value = false
|
setPattern = {
|
||||||
},
|
viewModel.currentPattern.value = it
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
||||||
items(viewModel.phoneNumberPatterns) { pattern ->
|
|
||||||
Text(
|
|
||||||
text = pattern.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp)
|
|
||||||
.clickable {
|
|
||||||
viewModel.chosenPattern.value = pattern
|
|
||||||
isSheetOpen.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+5
-6
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Single
|
|
||||||
|
|
||||||
data class FillProfileFormState(
|
data class FillProfileFormState(
|
||||||
val firstName: String = "",
|
val firstName: String = "",
|
||||||
@@ -95,7 +94,7 @@ class FillProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onPhoneChange(value: String) {
|
fun onPhoneChange(value: String) {
|
||||||
val maxDigits = chosenPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
val maxDigits = currentPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
||||||
_formStateFillProfile.update {
|
_formStateFillProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -153,14 +152,14 @@ class FillProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
val currentPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||||
|
|
||||||
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||||
|
|
||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
// Load default pattern
|
// Load default pattern
|
||||||
chosenPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
currentPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
|
|
||||||
// Load all phone number patterns
|
// Load all phone number patterns
|
||||||
phoneNumberPatterns.clear()
|
phoneNumberPatterns.clear()
|
||||||
@@ -173,7 +172,7 @@ class FillProfileViewModel(
|
|||||||
firstName = _formStateFillProfile.value.firstName,
|
firstName = _formStateFillProfile.value.firstName,
|
||||||
lastName = _formStateFillProfile.value.lastName,
|
lastName = _formStateFillProfile.value.lastName,
|
||||||
phone = _formStateFillProfile.value.phone,
|
phone = _formStateFillProfile.value.phone,
|
||||||
chosenPattern = chosenPattern.value?.mapToDomain()
|
chosenPattern = currentPattern.value?.mapToDomain()
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
@@ -187,7 +186,7 @@ class FillProfileViewModel(
|
|||||||
UpdateUserData(
|
UpdateUserData(
|
||||||
firstName = _formStateFillProfile.value.firstName,
|
firstName = _formStateFillProfile.value.firstName,
|
||||||
lastName = _formStateFillProfile.value.lastName,
|
lastName = _formStateFillProfile.value.lastName,
|
||||||
phone = chosenPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
phone = currentPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
||||||
convertNumberToPattern(
|
convertNumberToPattern(
|
||||||
phoneNumberPattern,
|
phoneNumberPattern,
|
||||||
_formStateFillProfile.value.phone
|
_formStateFillProfile.value.phone
|
||||||
|
|||||||
+46
-90
@@ -51,6 +51,8 @@ import com.prodhack.moscow2025.R
|
|||||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
@@ -118,31 +120,31 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.systemBarsPadding()
|
.systemBarsPadding()
|
||||||
.padding(horizontal = 30.dp),
|
.padding(horizontal = Paddings.large),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(Paddings.large))
|
||||||
Text(
|
Text(
|
||||||
text = "Профиль",
|
text = "Профиль",
|
||||||
style = typography.titleLarge,
|
style = typography.titleLarge,
|
||||||
fontSize = 40.sp
|
fontSize = 40.sp
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(Paddings.large))
|
||||||
TTTextField(
|
TTTextField(
|
||||||
value = formState.firstName,
|
value = formState.firstName,
|
||||||
onValueChange = viewModel::onFirstNameChange,
|
onValueChange = viewModel::onFirstNameChange,
|
||||||
label = "Имя",
|
label = "Имя",
|
||||||
error = formState.errors[AuthField.FirstName],
|
error = formState.errors[AuthField.FirstName],
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
TTTextField(
|
TTTextField(
|
||||||
value = formState.lastName,
|
value = formState.lastName,
|
||||||
onValueChange = viewModel::onLastNameChange,
|
onValueChange = viewModel::onLastNameChange,
|
||||||
label = "Фамилия",
|
label = "Фамилия",
|
||||||
error = formState.errors[AuthField.LastName],
|
error = formState.errors[AuthField.LastName],
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
TTTextField(
|
TTTextField(
|
||||||
value = formState.email,
|
value = formState.email,
|
||||||
onValueChange = viewModel::onEmailChange,
|
onValueChange = viewModel::onEmailChange,
|
||||||
@@ -150,75 +152,48 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
error = formState.errors[AuthField.Email],
|
error = formState.errors[AuthField.Email],
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
Row(
|
|
||||||
modifier = Modifier.height(IntrinsicSize.Min),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(Paddings.medium)
|
|
||||||
) {
|
|
||||||
FieldWrapper(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(IntrinsicSize.Min)
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.clip(Shapes.smallRoundedBox)
|
|
||||||
.background(
|
|
||||||
color = colorScheme.primary,
|
|
||||||
shape = Shapes.smallRoundedBox
|
|
||||||
),
|
|
||||||
value = viewModel.chosenPattern.value?.prefix ?: "",
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = colorScheme.onPrimary
|
|
||||||
),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { isSheetOpen.value = true }
|
|
||||||
.padding(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(15.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_arr_dropdown),
|
|
||||||
tint = colorScheme.onPrimary,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TTTextField(
|
TPhoneField(
|
||||||
value = formState.phone,
|
currentPattern = viewModel.chosenPattern.value,
|
||||||
onValueChange = viewModel::onPhoneChange,
|
currentPhone = formState.phone,
|
||||||
label = "Телефон",
|
onPhoneChange = viewModel::onPhoneChange,
|
||||||
keyboardOptions = KeyboardOptions(
|
error = formState.errors[AuthField.Phone],
|
||||||
keyboardType = KeyboardType.Phone
|
onOpenCountryList = {
|
||||||
),
|
isSheetOpen.value = true
|
||||||
visualTransformation = viewModel.chosenPattern.value?.pattern?.let {
|
|
||||||
PhoneVisualTransformation(it, '0')
|
|
||||||
} ?: VisualTransformation.None,
|
|
||||||
error = formState.errors[AuthField.Phone]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
if (viewModel.madeChanges.collectAsState().value) {
|
||||||
BigButton(
|
BigButton(
|
||||||
onClick = viewModel::submit,
|
onClick = viewModel::submit,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
buttonText = "Сохранить",
|
buttonText = "Сохранить",
|
||||||
isLoading = profileState is UIState.Loading
|
isLoading = profileState is UIState.Loading
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(15.dp))
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
Button(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = viewModel::reset,
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = colorScheme.secondaryContainer,
|
||||||
|
contentColor = colorScheme.onSecondaryContainer,
|
||||||
|
disabledContainerColor = colorScheme.secondaryContainer,
|
||||||
|
disabledContentColor = colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Отменить",
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
}
|
||||||
Button(
|
Button(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -250,34 +225,15 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSheetOpen.value) {
|
TPhoneCountryList(
|
||||||
ModalBottomSheet(
|
isSheetOpen = isSheetOpen,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = { isSheetOpen.value = false },
|
patternList = viewModel.phoneNumberPatterns,
|
||||||
) {
|
setPattern = {
|
||||||
Column(
|
viewModel.chosenPattern.value = it
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
||||||
items(viewModel.phoneNumberPatterns) { pattern ->
|
|
||||||
Text(
|
|
||||||
text = pattern.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp)
|
|
||||||
.clickable {
|
|
||||||
viewModel.chosenPattern.value = pattern
|
|
||||||
viewModel.onPhoneChange(formState.phone)
|
|
||||||
isSheetOpen.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-18
@@ -6,6 +6,8 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -28,8 +30,11 @@ import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
|||||||
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
||||||
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
@@ -78,8 +83,8 @@ class ProfileScreenViewModel(
|
|||||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||||
private val logOutUseCase: LogOutUseCase,
|
private val logOutUseCase: LogOutUseCase,
|
||||||
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||||
private val galleryRepository: GalleryRepository
|
galleryRepository: GalleryRepository
|
||||||
): BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
private val _formStateProfile = MutableStateFlow(ProfileState())
|
private val _formStateProfile = MutableStateFlow(ProfileState())
|
||||||
val formStateFillProfile: StateFlow<ProfileState> = _formStateProfile
|
val formStateFillProfile: StateFlow<ProfileState> = _formStateProfile
|
||||||
|
|
||||||
@@ -89,6 +94,28 @@ class ProfileScreenViewModel(
|
|||||||
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||||
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||||
|
|
||||||
|
private val realState = MutableStateFlow(_formStateProfile.value)
|
||||||
|
|
||||||
|
val madeChanges = _formStateProfile.combine(realState) { current, real ->
|
||||||
|
current.phone != real.phone ||
|
||||||
|
current.firstName != real.firstName ||
|
||||||
|
current.lastName != real.lastName ||
|
||||||
|
current.email != real.email
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
if (madeChanges.value) {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
email = realState.value.email,
|
||||||
|
phone = realState.value.phone,
|
||||||
|
firstName = realState.value.firstName,
|
||||||
|
lastName = realState.value.lastName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onEmailChange(value: String) {
|
fun onEmailChange(value: String) {
|
||||||
_formStateProfile.update {
|
_formStateProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -175,7 +202,9 @@ class ProfileScreenViewModel(
|
|||||||
|
|
||||||
fun submit() {
|
fun submit() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val pattern = chosenPattern.value
|
||||||
val validation = validateAuthFieldsUseCase.validateProfile(
|
val validation = validateAuthFieldsUseCase.validateProfile(
|
||||||
|
chosenPattern = pattern?.mapToDomain(),
|
||||||
firstName = _formStateProfile.value.firstName,
|
firstName = _formStateProfile.value.firstName,
|
||||||
lastName = _formStateProfile.value.lastName,
|
lastName = _formStateProfile.value.lastName,
|
||||||
email = _formStateProfile.value.email,
|
email = _formStateProfile.value.email,
|
||||||
@@ -184,15 +213,6 @@ class ProfileScreenViewModel(
|
|||||||
|
|
||||||
val errors = validation.errors.toMutableMap()
|
val errors = validation.errors.toMutableMap()
|
||||||
|
|
||||||
val pattern = chosenPattern.value
|
|
||||||
if (pattern == null) {
|
|
||||||
errors[AuthField.Phone] = "Выберите код страны"
|
|
||||||
} else {
|
|
||||||
val expectedDigits = pattern.pattern.count { it == '0' }
|
|
||||||
if (_formStateProfile.value.phone.length != expectedDigits) {
|
|
||||||
errors[AuthField.Phone] = "Номер должен содержать $expectedDigits цифр"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
_formStateProfile.update { it.copy(errors = errors) }
|
_formStateProfile.update { it.copy(errors = errors) }
|
||||||
@@ -214,6 +234,7 @@ class ProfileScreenViewModel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.map { it.id }.collectRequest(_profileState)
|
result.map { it.id }.collectRequest(_profileState)
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +244,7 @@ class ProfileScreenViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
fun update() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
loadPhonePatterns()
|
loadPhonePatterns()
|
||||||
|
|
||||||
@@ -233,12 +254,8 @@ class ProfileScreenViewModel(
|
|||||||
val selectedPattern = phoneNumberPatterns.firstOrNull { pattern ->
|
val selectedPattern = phoneNumberPatterns.firstOrNull { pattern ->
|
||||||
val codeDigits = pattern.countryCode.filter { it.isDigit() }
|
val codeDigits = pattern.countryCode.filter { it.isDigit() }
|
||||||
digits.startsWith(codeDigits) && digits.length >= codeDigits.length
|
digits.startsWith(codeDigits) && digits.length >= codeDigits.length
|
||||||
} ?: phoneNumberPatterns.firstOrNull {
|
} ?: getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
it.countryCodeISO.equals(
|
?: phoneNumberPatterns.firstOrNull()
|
||||||
getDefaultPhoneNumberPatternUseCase.execute()?.countryCodeISO,
|
|
||||||
ignoreCase = true
|
|
||||||
)
|
|
||||||
} ?: phoneNumberPatterns.firstOrNull()
|
|
||||||
|
|
||||||
selectedPattern?.let { chosenPattern.value = it }
|
selectedPattern?.let { chosenPattern.value = it }
|
||||||
|
|
||||||
@@ -257,10 +274,15 @@ class ProfileScreenViewModel(
|
|||||||
phone = digitsWithoutCode.take(maxDigits)
|
phone = digitsWithoutCode.take(maxDigits)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
realState.emit(_formStateProfile.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadPhonePatterns() {
|
private fun loadPhonePatterns() {
|
||||||
phoneNumberPatterns.clear()
|
phoneNumberPatterns.clear()
|
||||||
phoneNumberPatterns.addAll(
|
phoneNumberPatterns.addAll(
|
||||||
|
|||||||
Reference in New Issue
Block a user