From 44d7860883a9480be1dc21083d0c2ed61ab39e2d Mon Sep 17 00:00:00 2001 From: dany Date: Fri, 21 Nov 2025 18:39:00 +0300 Subject: [PATCH] feat: added filling profile info after registration --- .idea/deploymentTargetSelector.xml | 8 + .../usecase/auth/ValidateAuthFieldsUseCase.kt | 8 +- .../presentation/navigation/AppDestination.kt | 1 + .../presentation/navigation/TTasksNavHost.kt | 16 +- .../screens/fillProfile/FillProfileScreen.kt | 158 +++++++++++++++++- .../fillProfile/FillProfileViewModel.kt | 29 +--- .../presentation/screens/login/LoginScreen.kt | 2 +- .../screens/register/RegisterScreen.kt | 31 ++-- 8 files changed, 205 insertions(+), 48 deletions(-) diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..4c55468 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt index d176516..9e5ea3c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt @@ -5,7 +5,7 @@ import org.koin.core.annotation.Single enum class AuthField { FirstName, - SecondName, + LastName, Email, Password, ConfirmPassword, @@ -24,15 +24,13 @@ data class ValidationResult( class ValidateAuthFieldsUseCase { fun validateFillProfile( - displayName: String, firstName: String, lastName: String, phone: String ): ValidationResult { val errors = buildMap { - if (displayName.isBlank()) put(AuthField.FirstName, "Введите никнейм") if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") - if (lastName.isBlank()) put(AuthField.SecondName, "Введите фамилию") + if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию") if (!isPhoneValid(phone)) put(AuthField.Phone, "Некорректный номер телефона") } return ValidationResult(errors) @@ -89,7 +87,7 @@ class ValidateAuthFieldsUseCase { ): ValidationResult { val errors = buildMap { if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") - if (secondName.isBlank()) put(AuthField.SecondName, "Введите фамилию") + if (secondName.isBlank()) put(AuthField.LastName, "Введите фамилию") } return ValidationResult(errors) } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt index cd12537..e9e9c4a 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt @@ -15,4 +15,5 @@ sealed class AppDestination(val route: String) { data object Profile : AppDestination("app/profile") + data object FillProfile : AppDestination("app/fill_profile") } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt index 38b5a82..988e0c6 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt @@ -9,6 +9,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.prodhack.moscow2025.presentation.screens.main.MainScreen import com.prodhack.moscow2025.domain.utils.NetworkError +import com.prodhack.moscow2025.presentation.screens.fillProfile.FillProfileScreen import com.prodhack.moscow2025.presentation.screens.login.LoginScreen import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen @@ -64,7 +65,7 @@ fun TTasksNavHost( navController.popBackStack() }, onSuccess = { - navController.navigate(AppDestination.Main.route) { + navController.navigate(AppDestination.FillProfile.route) { popUpTo(AppDestination.Register.route) { inclusive = true } @@ -73,6 +74,19 @@ fun TTasksNavHost( ) } + composable(AppDestination.FillProfile.route) { + FillProfileScreen( + snackbarHostState = snackbarHostState, + onSuccess = { + navController.navigate(AppDestination.Main.route) { + popUpTo(AppDestination.FillProfile.route) { + inclusive = true + } + } + } + ) + } + composable(AppDestination.Main.route) { MainScreen() } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt index fee1d41..8bbd9fe 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt @@ -1,9 +1,163 @@ package com.prodhack.moscow2025.presentation.screens.fillProfile +import androidx.compose.foundation.Image +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.presentation.components.standart.BigButton +import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField +import com.prodhack.moscow2025.presentation.components.standart.TTTextField +import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope +import com.prodhack.moscow2025.presentation.utils.UIState +import org.koin.androidx.compose.koinViewModel @Composable -fun FillProfileScreen() { - Text("Fill profile will be here soon :)") +fun ErrorCollectorScope.FillProfileScreen( + snackbarHostState: SnackbarHostState, + onSuccess: () -> Unit, + viewModel: FillProfileViewModel = koinViewModel() +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + val formState by viewModel.formStateFillProfile.collectAsState() + + var errorText by remember { mutableStateOf("") } + val fillProfileState by viewModel.profileFillState.collectAsStateWithCallbacks( + onInputError = { + errorText = it.error + }, + onConnectionError = { + errorText = "Нет подключения к сети" + }, + onUnexpectedError = { + errorText = it.error + }, + onLoading = { + errorText = "" + }, + onSuccess = { + errorText = "" + } + ) + + LaunchedEffect(fillProfileState) { + if (fillProfileState is UIState.Success) { + onSuccess() + } + } + + LaunchedEffect(errorText) { + if (errorText.isNotEmpty()) { + snackbarHostState.showSnackbar( + message = "Ошибка: $errorText", + duration = SnackbarDuration.Short + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomStart + ) { + Image( + painter = painterResource(R.drawable.lottie), + contentDescription = null, + modifier = Modifier.width(130.dp), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 30.dp, end = 30.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Давайте\nзнакомиться!", + style = typography.titleLarge, + fontSize = 31.sp + ) + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.size(140.dp), + contentScale = ContentScale.Crop + ) + } + Spacer(Modifier.height(20.dp)) + TTTextField( + value = formState.firstName, + onValueChange = viewModel::onFirstNameChange, + label = "Ваше имя", + error = formState.errors[AuthField.FirstName] + ) + Spacer(Modifier.height(12.dp)) + TTTextField( + value = formState.lastName, + onValueChange = viewModel::onLastNameChange, + label = "Ваша фамилия", + error = formState.errors[AuthField.LastName] + ) + Spacer(Modifier.height(12.dp)) + TTTextField( + value = formState.phone, + onValueChange = viewModel::onPhoneChange, + label = "Ваш телефон", + error = formState.errors[AuthField.Phone] + ) + Spacer(modifier = Modifier.height(20.dp)) + BigButton( + onClick = viewModel::submit, + modifier = Modifier.fillMaxWidth(), + buttonText = "Сохранить данные", + isLoading = fillProfileState is UIState.Loading + ) + Spacer(modifier = Modifier.height(80.dp)) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt index df97251..aac1dc3 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt @@ -23,9 +23,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Single data class FillProfileFormState( - val displayName: String = "", val firstName: String = "", val lastName: String = "", val phone: String = "", @@ -38,7 +39,6 @@ data class FillProfileFormState( other as FillProfileFormState - if (displayName != other.displayName) return false if (firstName != other.firstName) return false if (lastName != other.lastName) return false if (phone != other.phone) return false @@ -49,8 +49,7 @@ data class FillProfileFormState( } override fun hashCode(): Int { - var result = displayName.hashCode() - result = 31 * result + firstName.hashCode() + var result = firstName.hashCode() result = 31 * result + lastName.hashCode() result = 31 * result + phone.hashCode() result = 31 * result + (avatar?.contentHashCode() ?: 0) @@ -59,33 +58,23 @@ data class FillProfileFormState( } } +@KoinViewModel class FillProfileViewModel( private val updateUserUseCase: UpdateUserUseCase, private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, private val galleryRepository: GalleryRepository ) : BaseViewModel() { private val _formStateFillProfile = MutableStateFlow(FillProfileFormState()) - val formStateSignUp: StateFlow = _formStateFillProfile - + val formStateFillProfile: StateFlow = _formStateFillProfile private val _profileFillState = MutableUIStateFlow() val profileFillState: StateFlow> = _profileFillState - - fun onDisplayNameChange(value: String) { - _formStateFillProfile.update { - it.copy( - displayName = value, - errors = it.errors - AuthField.Email - ) - } - } - fun onFirstNameChange(value: String) { _formStateFillProfile.update { it.copy( firstName = value, - errors = it.errors - AuthField.Email + errors = it.errors - AuthField.FirstName ) } } @@ -94,7 +83,7 @@ class FillProfileViewModel( _formStateFillProfile.update { it.copy( lastName = value, - errors = it.errors - AuthField.Email + errors = it.errors - AuthField.LastName ) } } @@ -103,7 +92,7 @@ class FillProfileViewModel( _formStateFillProfile.update { it.copy( phone = value, - errors = it.errors - AuthField.Email + errors = it.errors - AuthField.Phone ) } } @@ -158,7 +147,6 @@ class FillProfileViewModel( fun submit() { viewModelScope.launch { val validation = validateAuthFieldsUseCase.validateFillProfile( - displayName = _formStateFillProfile.value.displayName, firstName = _formStateFillProfile.value.firstName, lastName = _formStateFillProfile.value.lastName, phone = _formStateFillProfile.value.phone @@ -173,7 +161,6 @@ class FillProfileViewModel( val result = updateUserUseCase( UpdateUserData( - displayName = _formStateFillProfile.value.displayName, firstName = _formStateFillProfile.value.firstName, lastName = _formStateFillProfile.value.lastName, phone = _formStateFillProfile.value.phone diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt index e7f1dc1..281b02f 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt @@ -131,7 +131,7 @@ fun ErrorCollectorScope.LoginScreen( painter = painterResource(R.drawable.ic_launcher_foreground), contentDescription = null, modifier = Modifier - .size(250.dp) + .size(200.dp) .noRippleClickable { showDialog.value = true } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt index 1bf160a..d321fda 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt @@ -45,6 +45,7 @@ import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField import com.prodhack.moscow2025.presentation.components.standart.TTTextField import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable import org.koin.androidx.compose.koinViewModel @Composable @@ -114,24 +115,18 @@ fun ErrorCollectorScope.RegisterScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Давайте\nзнакомиться!", - style = typography.titleLarge, - fontSize = 31.sp - ) - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), - contentDescription = null, - modifier = Modifier.size(140.dp), - contentScale = ContentScale.Crop - ) - } - Spacer(Modifier.height(20.dp)) + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(200.dp) + ) + Text( + text = "Регистрация", + style = MaterialTheme.typography.titleLarge, + fontSize = 40.sp + ) + Spacer(modifier = Modifier.height(10.dp)) TTTextField( value = formState.email, onValueChange = viewModel::onEmailChange,