From 5084dedf90760c24d67596cd5537f79043cb5ece Mon Sep 17 00:00:00 2001 From: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:05:54 +0300 Subject: [PATCH] feat: add simple version of form --- app/build.gradle.kts | 1 + .../moscow2025/data/dto/ResumeDtos.kt | 37 ++++ .../ResumeRepositoryImpl.kt | 30 ++++ .../interfaces/resumes/ResumeRepository.kt | 4 + .../prodhack/moscow2025/domain/models/Auth.kt | 11 +- .../moscow2025/domain/models/ResumeModel.kt | 14 ++ ...ldsUseCase.kt => ValidateFieldsUseCase.kt} | 120 ++++++------- .../usecase/resumes/CreateResumeUseCase.kt | 12 ++ .../usecase/resumes/SuggestSkillsUseCase.kt | 28 +++ .../components/standart/TBubble.kt | 78 +++++++++ .../components/standart/TTTextField.kt | 2 +- .../presentation/navigation/AppDestination.kt | 2 + .../presentation/navigation/TTasksNavHost.kt | 19 +- .../createResume/CreateResumeScreen.kt | 162 ++++++++++++++++++ .../createResume/CreateResumeViewModel.kt | 149 ++++++++++++++++ .../screens/fillProfile/FillProfileScreen.kt | 21 +-- .../fillProfile/FillProfileViewModel.kt | 8 +- .../presentation/screens/login/LoginScreen.kt | 2 +- .../screens/login/LoginViewModel.kt | 8 +- .../presentation/screens/main/MainScreen.kt | 15 +- .../screens/main/MainScreenViewModel.kt | 1 - .../screens/profile/ProfileScreen.kt | 16 +- .../screens/profile/ProfileScreenViewModel.kt | 22 +-- .../screens/register/RegisterScreen.kt | 2 +- .../screens/register/RegisterViewModel.kt | 35 +--- .../moscow2025/presentation/utils/UIState.kt | 5 +- app/src/main/res/drawable/ic_remove.xml | 16 ++ gradle/libs.versions.toml | 2 + 28 files changed, 661 insertions(+), 161 deletions(-) rename app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/{ValidateAuthFieldsUseCase.kt => ValidateFieldsUseCase.kt} (60%) create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/SuggestSkillsUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TBubble.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt create mode 100644 app/src/main/res/drawable/ic_remove.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 91bc919..7788102 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -85,6 +85,7 @@ dependencies { implementation(libs.compose.animation.graphics) implementation(libs.material.icons.extended) implementation(libs.androidx.foundation) + implementation(libs.androidx.runtime) androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.ui.tooling) diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt index 15f70e8..36ccca7 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt @@ -2,6 +2,7 @@ package com.prodhack.moscow2025.data.dto import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity import com.prodhack.moscow2025.domain.models.ExperienceType +import com.prodhack.moscow2025.domain.models.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeModel import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -32,6 +33,14 @@ enum class ExperienceTypeDTO { } } +fun ExperienceType.mapToData(): ExperienceTypeDTO = when (this) { + ExperienceType.NoExperience -> ExperienceTypeDTO.NoExperience + ExperienceType.LessThan1 -> ExperienceTypeDTO.LessThan1 + ExperienceType.Between1And3 -> ExperienceTypeDTO.Between1And3 + ExperienceType.Between3And6 -> ExperienceTypeDTO.Between3And6 + ExperienceType.MoreThan6 -> ExperienceTypeDTO.MoreThan6 +} + @Serializable data class ResumeDTO( val id: String, @@ -69,6 +78,23 @@ data class ResumeDTO( ) } +@Serializable +data class ResumeCreateDTO( + @SerialName("about_me") + val aboutMe: String, + @SerialName("experience_type") + val experienceType: ExperienceTypeDTO, + val keySkills: List, + val position: String +) + +fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO( + aboutMe = about, + experienceType = experienceType.mapToData(), + keySkills = skills, + position = position +) + @Serializable data class PredictionDTO( @SerialName("from_salary") @@ -82,4 +108,15 @@ data class PredictionDTO( @Serializable data class ResumeListDTO( val resumes: List +) + +@Serializable +data class ResumeIdDTO( + @SerialName("resume_id") + val resumeId: String +) + +@Serializable +data class ResumeSkillDTO( + val name: String ) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt index 553ca44..52bc2c6 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt @@ -4,12 +4,21 @@ import androidx.paging.map import com.prodhack.moscow2025.data.base.BaseRepository import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase +import com.prodhack.moscow2025.data.dto.ResumeCreateDTO +import com.prodhack.moscow2025.data.dto.ResumeIdDTO import com.prodhack.moscow2025.data.dto.ResumeListDTO +import com.prodhack.moscow2025.data.dto.ResumeSkillDTO import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import com.prodhack.moscow2025.domain.models.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper +import io.ktor.client.request.setBody import io.ktor.client.request.url +import io.ktor.http.ContentType import io.ktor.http.HttpMethod +import io.ktor.http.contentType +import io.ktor.http.parameters +import io.ktor.http.path import kotlinx.coroutines.flow.map import org.koin.core.annotation.Single @@ -37,4 +46,25 @@ class ResumeRepositoryImpl( }.map { it -> it.resumes.map { it.mapToDB() } } } ).map { it -> it.map { it.mapToDomain() } } + + override suspend fun suggestSkills(query: String): Result> = + networkRequest> { + method = HttpMethod.Get + url { + path("key_skills") + parameters.append("query", query) + } + }.map { it.map { it.name } } + + override suspend fun createResume(resumeForm: ResumeCreationModel): Result = + networkRequest { + method = HttpMethod.Post + + url { + url("/resume") + } + + setBody(ResumeCreateDTO) + contentType(ContentType.Application.Json) + }.map { it.resumeId } } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt index 6fbe291..ebae7f2 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt @@ -1,8 +1,12 @@ package com.prodhack.moscow2025.domain.interfaces.resumes +import com.prodhack.moscow2025.domain.models.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper interface ResumeRepository { fun loadResumeList(): RemotePagingWrapper + + suspend fun suggestSkills(query: String): Result> + suspend fun createResume(resumeForm: ResumeCreationModel): Result } diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt index 0eef4f8..59f219c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt @@ -8,4 +8,13 @@ data class RegisterData( data class LoginData( val email: String, val password: String -) \ No newline at end of file +) + +enum class AuthField { + FirstName, + LastName, + Email, + Password, + ConfirmPassword, + Phone +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt index 967beeb..a3636cf 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt @@ -10,10 +10,24 @@ data class ResumeModel( val recommendedSkills: List ) +data class ResumeCreationModel( + val position: String, + val about: String, + val skills: List, + val experienceType: ExperienceType +) + enum class ExperienceType { NoExperience, LessThan1, Between1And3, Between3And6, MoreThan6 +} + +enum class ResumeField { + About, + Position, + Experience, + KeySkills } \ No newline at end of file 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/ValidateFieldsUseCase.kt similarity index 60% rename from app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt rename to app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateFieldsUseCase.kt index 58684c3..72ad2d3 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/ValidateFieldsUseCase.kt @@ -1,55 +1,48 @@ package com.prodhack.moscow2025.domain.usecase.auth -import android.util.Log import android.util.Patterns +import com.prodhack.moscow2025.domain.models.AuthField +import com.prodhack.moscow2025.domain.models.ExperienceType import com.prodhack.moscow2025.domain.models.PhoneNumberPattern +import com.prodhack.moscow2025.domain.models.ResumeField +import com.prodhack.moscow2025.presentation.screens.createResume.UIExperience import org.koin.core.annotation.Single -enum class AuthField { - FirstName, - LastName, - Email, - Password, - ConfirmPassword, - Phone -} - - -data class ValidationResult( - val errors: Map = emptyMap() +data class ValidationResult( + val errors: Map = emptyMap() ) { val isValid: Boolean get() = errors.isEmpty() } @Single -class ValidateAuthFieldsUseCase { - fun validateProfile( - chosenPattern: PhoneNumberPattern?, - firstName: String, - lastName: String, - email: String, - phone: String - ): ValidationResult { - val errors = buildMap { - if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") - if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию") - if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") - val maxCount = chosenPattern!!.pattern.count { it == '0' } - if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put( - AuthField.Phone, - "Некорректный номер телефона" - ) - } - return ValidationResult(errors) - } +class ValidateFieldsUseCase { + fun validateProfile( + chosenPattern: PhoneNumberPattern?, + firstName: String, + lastName: String, + email: String, + phone: String + ): ValidationResult { + val errors = buildMap { + if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") + if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию") + if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") + val maxCount = chosenPattern!!.pattern.count { it == '0' } + if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put( + AuthField.Phone, + "Некорректный номер телефона" + ) + } + return ValidationResult(errors) + } fun validateFillProfile( chosenPattern: PhoneNumberPattern?, firstName: String, lastName: String, phone: String - ): ValidationResult { + ): ValidationResult { val errors = buildMap { if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию") @@ -66,7 +59,7 @@ class ValidateAuthFieldsUseCase { email: String, password: String, confirmPassword: String - ): ValidationResult { + ): ValidationResult { val errors = buildMap { if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") validatePassword(password)?.let { put(AuthField.Password, it) } @@ -79,6 +72,40 @@ class ValidateAuthFieldsUseCase { return ValidationResult(errors) } + fun validateLogin( + email: String, + password: String + ): ValidationResult { + val errors = buildMap { + if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") + validatePassword(password)?.let { put(AuthField.Password, it) } + } + return ValidationResult(errors) + } + + + fun validateResume( + about: String, + position: String, + experience: ExperienceType?, + keySkills: List + ): ValidationResult { + val errors = buildMap { + if (about.isBlank()) put(ResumeField.About, "Без этого мы не сможем рассчитать вашу ЗП") + if (position.isBlank()) put( + ResumeField.Position, + "Без этого мы не сможем рассчитать вашу ЗП" + ) + if (experience == null) put( + ResumeField.Experience, + "Без этого мы не сможем рассчитать вашу ЗП" + ) + if (keySkills.isEmpty()) put(ResumeField.KeySkills, "Укажите хотя бы один навык") + + } + return ValidationResult(errors) + } + fun validatePassword(password: String): String? { if (password.length < 8) { return "Пароль должен быть не менее 8 символов" @@ -95,29 +122,6 @@ class ValidateAuthFieldsUseCase { return null } - - fun validateLogin( - email: String, - password: String - ): ValidationResult { - val errors = buildMap { - if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") - validatePassword(password)?.let { put(AuthField.Password, it) } - } - return ValidationResult(errors) - } - - fun validateProfile( - firstName: String, - secondName: String, - ): ValidationResult { - val errors = buildMap { - if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") - if (secondName.isBlank()) put(AuthField.LastName, "Введите фамилию") - } - return ValidationResult(errors) - } - private fun isEmailValid(email: String): Boolean = email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches() diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt new file mode 100644 index 0000000..5a80e26 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt @@ -0,0 +1,12 @@ +package com.prodhack.moscow2025.domain.usecase.resumes + +import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import com.prodhack.moscow2025.domain.models.ResumeCreationModel +import org.koin.core.annotation.Single + +@Single +class CreateResumeUseCase( + private val resumeRepository: ResumeRepository +){ + suspend operator fun invoke(resumeForm: ResumeCreationModel): Result = resumeRepository.createResume(resumeForm) +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/SuggestSkillsUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/SuggestSkillsUseCase.kt new file mode 100644 index 0000000..c6501b6 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/SuggestSkillsUseCase.kt @@ -0,0 +1,28 @@ +package com.prodhack.moscow2025.domain.usecase.resumes + +import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import org.koin.core.annotation.Single + + +@Single +class SuggestSkillsUseCase( + private val resumeRepository: ResumeRepository +) { + suspend operator fun invoke(query: String): Result> = + resumeRepository.suggestSkills(query = query) + + // mock +// suspend operator fun invoke(query: String): Result> = +// Result.success(listOf( +// "Python", "Kotlin", "Java", "C#", "JavaScript", +// "TypeScript", "Go", "Rust", "Swift", "PHP", +// "Ruby", "C++", "Dart", "HTML", "CSS", +// "SQL", "NoSQL", "MongoDB", "PostgreSQL", "MySQL", +// "Docker", "Kubernetes", "AWS", "Azure", "Google Cloud Platform", +// "React", "Angular", "Vue.js", "Node.js", "Spring Boot", +// "Django", "Flask", "ASP.NET", "Ruby on Rails", "Laravel", +// "Android", "iOS", "Flutter", "React Native", "Xamarin", +// "Git", "Jira", "Confluence", "Jenkins", "Travis CI", +// "Agile", "Scrum", "Kanban", "DevOps", "GraphQL" +// ).filter { it.contains(query, ignoreCase = true) }.take(10)) +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TBubble.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TBubble.kt new file mode 100644 index 0000000..4047d0f --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TBubble.kt @@ -0,0 +1,78 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.theme.Paddings +import com.prodhack.moscow2025.presentation.theme.Shapes +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable + +@Composable +fun TBubble( + modifier: Modifier = Modifier, + text: String, + isSelected: Boolean = false, + onDelete: (() -> Unit)? = null +) { + val shapes = MaterialTheme.shapes + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + val bubbleColor = + animateColorAsState( + if (isSelected) colorScheme.primary else colorScheme.secondary + ) + val bubbleBorderColor = + animateColorAsState( + if (isSelected) colorScheme.primaryFixed else colorScheme.secondaryFixed + ) + val contentColor = + animateColorAsState( + if (isSelected) colorScheme.onPrimary else colorScheme.onSecondary + ) + Row( + modifier = modifier + .background( + color = bubbleColor.value, + shape = shapes.small + ) + .border( + width = 1.dp, + color = bubbleBorderColor.value, + shape = shapes.small + ) + .clip(Shapes.smallRoundedBox) + .padding(horizontal = Paddings.small, vertical = Paddings.verySmall), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = typography.labelLarge, + fontSize = 16.sp, + color = contentColor.value + ) + onDelete?.let { + Spacer(modifier = Modifier.width(Paddings.verySmall)) + Icon( + modifier = Modifier.noRippleClickable(it), + painter = painterResource(R.drawable.ic_remove), + tint = contentColor.value, + contentDescription = "Remove" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt index 3b2d70a..c43dcb0 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt @@ -266,7 +266,7 @@ fun TTTextFieldWithSearch( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit = {}, - readOnly: Boolean = true, + readOnly: Boolean = false, label: String, error: String? = null, singleLine: Boolean = true, 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 74daa21..d4d2e02 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 @@ -20,5 +20,7 @@ sealed class AppDestination(val route: String) { data object ResumeDetails : AppDestination("resume/details") { const val ARG_ID = "id" } + + data object ResumeCreation: AppDestination("resume/creation") } 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 af7827e..effffa0 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 @@ -11,6 +11,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.createResume.CreateResumeScreen import com.prodhack.moscow2025.presentation.screens.fillProfile.FillProfileScreen import com.prodhack.moscow2025.presentation.screens.login.LoginScreen import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen @@ -91,11 +92,15 @@ fun TTasksNavHost( } composable(AppDestination.Main.route) { - MainScreen(openResumeDetails = { id -> - navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply { - putString(AppDestination.ResumeDetails.ARG_ID, id) - }) - }) + MainScreen( + openResumeDetails = { id -> + navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply { + putString(AppDestination.ResumeDetails.ARG_ID, id) + }) + }, openCreateResume = { + navController.navigate(AppDestination.ResumeCreation.route) + } + ) } composable(AppDestination.Profile.route) @@ -111,6 +116,10 @@ fun TTasksNavHost( composable(AppDestination.ResumeDetails.route) { ResumeDetailsScreen(navBackStackEntry = it) } + + composable(AppDestination.ResumeCreation.route) { + CreateResumeScreen() + } } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt new file mode 100644 index 0000000..1ebc920 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt @@ -0,0 +1,162 @@ +package com.prodhack.moscow2025.presentation.screens.createResume + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +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.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.domain.models.ResumeField +import com.prodhack.moscow2025.presentation.components.standart.BigButton +import com.prodhack.moscow2025.presentation.components.standart.TBubble +import com.prodhack.moscow2025.presentation.components.standart.TTTextField +import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithDropdown +import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch +import com.prodhack.moscow2025.presentation.theme.Paddings +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable +import org.koin.androidx.compose.koinViewModel + + +@Composable +fun CreateResumeScreen( + viewModel: CreateResumeViewModel = koinViewModel() +) { + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + + val formState = viewModel.formStateFillResume.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = Paddings.large) + ) { + Spacer(modifier = Modifier.height(Paddings.large)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .rotate(180f) + .size(24.dp), + painter = painterResource(R.drawable.ic_arr_details), + tint = colorScheme.onBackground, + contentDescription = "go back" + ) + Text(text = "Новое резюме", style = typography.titleLarge, fontSize = 24.sp) + Spacer(modifier = Modifier.size(24.dp)) + } + Spacer(modifier = Modifier.height(Paddings.large)) + TTTextField( + value = formState.value.position, + onValueChange = viewModel::onPositionChange, + label = "Какая должность вас интересует?", + error = formState.value.errors[ResumeField.Position] + ) + Spacer(modifier = Modifier.height(Paddings.medium)) + TTTextField( + value = formState.value.about, + onValueChange = viewModel::onAboutChange, + singleLine = false, + maxLines = Int.MAX_VALUE, + label = "Расскажите о себе", + error = formState.value.errors[ResumeField.Position] + ) + Spacer(modifier = Modifier.height(Paddings.medium)) + TTTextFieldWithDropdown( + value = formState.value.experience?.friendlyName ?: "", + onValueChange = {}, + singleLine = false, + maxLines = Int.MAX_VALUE, + label = "Какой у вас опыт в данной сфере?", + error = formState.value.errors[ResumeField.Experience], + dropdownItems = viewModel.experienceOptions, + dropDownItem = { + Text(text = it.friendlyName, style = typography.titleMedium, fontSize = 16.sp) + }, + onDropdownItemSelected = viewModel::onExperienceSelect + ) + Spacer(modifier = Modifier.height(Paddings.medium)) + TTTextFieldWithSearch( + value = viewModel.skillSearchQuery.value, + onValueChange = { + viewModel.skillSearchQuery.value = it + }, + label = "Ваши навыки", + error = formState.value.errors[ResumeField.Experience], + dropdownItems = viewModel.suggestedSkills.collectAsState(emptyList()).value, + dropDownItem = { + Text(text = it, style = typography.titleMedium, fontSize = 16.sp) + }, + onDropdownItemSelected = viewModel::onAddSkill, + trailingIcon = { + if (viewModel.skillSearchQuery.value.isNotBlank()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.noRippleClickable { + viewModel.onAddSkill(viewModel.skillSearchQuery.value) + } + ) { + Text( + "Добавить", + style = typography.labelLarge, + fontSize = 12.sp, + color = colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(Paddings.verySmall)) + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(R.drawable.ic_plus), + tint = colorScheme.onPrimary, + contentDescription = null + ) + Spacer(modifier = Modifier.width(Paddings.medium)) + + } + } + } + ) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + Paddings.small + ), + verticalArrangement = Arrangement.spacedBy( + Paddings.small + ) + ) { + formState.value.keySkills.forEach { skillName -> + TBubble(text = skillName) { + viewModel.onRemoveSkill(skillName) + } + } + } + + Spacer(modifier = Modifier.height(Paddings.large)) + + BigButton( + onClick = {}, + buttonText = "Узнать свою ЗП", + isLoading = viewModel.resumeFillState.collectAsState().value.isLoading + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt new file mode 100644 index 0000000..f356967 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt @@ -0,0 +1,149 @@ +package com.prodhack.moscow2025.presentation.screens.createResume + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.viewModelScope +import com.prodhack.moscow2025.domain.models.ExperienceType +import com.prodhack.moscow2025.domain.models.RegisterData +import com.prodhack.moscow2025.domain.models.ResumeCreationModel +import com.prodhack.moscow2025.domain.models.ResumeField +import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.CreateResumeUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow +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 kotlin.math.exp + +data class ResumeFormState( + val about: String = "", + val position: String = "", + val experience: UIExperience? = null, + val keySkills: Set = emptySet(), + val errors: Map = emptyMap() +) + +sealed class UIExperience(val friendlyName: String) { + data object NoExperience : UIExperience("Без опыта") + data object LessThan1 : UIExperience("Меньше года") + data object Between1And3 : UIExperience("От 1 до 3 лет") + data object Between3And6 : UIExperience("От 3 до 6 лет") + data object MoreThan6 : UIExperience("Более 6 лет") + + fun mapToDomain(): ExperienceType = + when (this) { + is NoExperience -> ExperienceType.NoExperience + is LessThan1 -> ExperienceType.LessThan1 + is Between1And3 -> ExperienceType.Between1And3 + is Between3And6 -> ExperienceType.Between3And6 + is MoreThan6 -> ExperienceType.MoreThan6 + } +} + +@KoinViewModel +class CreateResumeViewModel( + private val suggestSkillsUseCase: SuggestSkillsUseCase, + private val validateDataUseCase: ValidateFieldsUseCase, + private val createResumeUseCase: CreateResumeUseCase +) : BaseViewModel() { + private val _formStateFillResume = MutableStateFlow(ResumeFormState()) + val formStateFillResume: StateFlow = _formStateFillResume + + private val _resumeFillState = MutableUIStateFlow() + val resumeFillState: StateFlow> = _resumeFillState + + fun onAboutChange(value: String) { + _formStateFillResume.update { + it.copy( + about = value, + errors = it.errors - ResumeField.About + ) + } + } + + fun onPositionChange(value: String) { + _formStateFillResume.update { + it.copy( + position = value, + errors = it.errors - ResumeField.Position + ) + } + } + + fun onExperienceSelect(value: UIExperience) { + _formStateFillResume.update { + it.copy( + experience = value, + errors = it.errors - ResumeField.Experience + ) + } + } + + fun onAddSkill(value: String) { + _formStateFillResume.update { + it.copy( + keySkills = it.keySkills + value, + errors = it.errors - ResumeField.KeySkills + ) + } + } + + fun onRemoveSkill(value: String) { + _formStateFillResume.update { + it.copy( + keySkills = it.keySkills - value, + errors = it.errors - ResumeField.KeySkills + ) + } + } + + val skillSearchQuery = mutableStateOf("") + + val suggestedSkills = snapshotFlow { skillSearchQuery.value }.map { + suggestSkillsUseCase(it).getOrNull() ?: emptyList() + } + + val experienceOptions = listOf( + UIExperience.NoExperience, + UIExperience.LessThan1, + UIExperience.Between1And3, + UIExperience.Between3And6, + UIExperience.MoreThan6 + ) + + fun submit() { + viewModelScope.launch { + val validation = validateDataUseCase.validateResume( + about = _formStateFillResume.value.about, + position = _formStateFillResume.value.position, + experience = _formStateFillResume.value.experience?.mapToDomain(), + keySkills = _formStateFillResume.value.keySkills.toList(), + ) + + if (!validation.isValid) { + _formStateFillResume.update { it.copy(errors = validation.errors) } + return@launch + } + + _resumeFillState.emit(UIState.Loading()) + + val result = createResumeUseCase( + with(_formStateFillResume.value) { + ResumeCreationModel( + position = position, + about = about, + skills = keySkills.toList(), + experienceType = experience!!.mapToDomain() + ) + } + + ) + result.collectRequest(_resumeFillState) + } + } +} \ No newline at end of file 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 240aa4b..3626b5a 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,35 +1,24 @@ package com.prodhack.moscow2025.presentation.screens.fillProfile -import android.util.Log import androidx.compose.foundation.Image -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.Spacer -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.imePadding -import androidx.compose.foundation.layout.offset 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -43,25 +32,17 @@ 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.clip import androidx.compose.ui.layout.ContentScale 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 androidx.compose.ui.unit.sp import com.prodhack.moscow2025.R -import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.presentation.components.standart.BigButton -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.theme.Paddings -import com.prodhack.moscow2025.presentation.theme.Shapes import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope -import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation import com.prodhack.moscow2025.presentation.utils.UIState import org.koin.androidx.compose.koinViewModel 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 cb94799..5e0dcae 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 @@ -14,11 +14,11 @@ import coil.ImageLoader import coil.request.ImageRequest import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider import com.prodhack.moscow2025.domain.interfaces.GalleryRepository +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.domain.models.UpdateUserData import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase -import com.prodhack.moscow2025.domain.usecase.auth.AuthField import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase -import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern @@ -65,7 +65,7 @@ data class FillProfileFormState( @KoinViewModel class FillProfileViewModel( private val updateUserUseCase: UpdateUserUseCase, - private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, + private val validateFieldsUseCase: ValidateFieldsUseCase, private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase, private val galleryRepository: GalleryRepository ) : BaseViewModel() { @@ -168,7 +168,7 @@ class FillProfileViewModel( fun submit() { viewModelScope.launch { - val validation = validateAuthFieldsUseCase.validateFillProfile( + val validation = validateFieldsUseCase.validateFillProfile( 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 0bcf9e2..992bd66 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 @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import com.prodhack.moscow2025.R -import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.models.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 diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt index a75cd2c..f8f5494 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt @@ -1,10 +1,10 @@ package com.prodhack.moscow2025.presentation.screens.login import androidx.lifecycle.viewModelScope +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.domain.models.LoginData -import com.prodhack.moscow2025.domain.usecase.auth.AuthField import com.prodhack.moscow2025.domain.usecase.auth.LoginUserUseCase -import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -22,7 +22,7 @@ data class LoginFormState( @KoinViewModel class LoginViewModel( private val loginUserUseCase: LoginUserUseCase, - private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase + private val validateFieldsUseCase: ValidateFieldsUseCase ) : BaseViewModel() { private val _formState = MutableStateFlow(LoginFormState()) @@ -41,7 +41,7 @@ class LoginViewModel( fun submit() { viewModelScope.launch { - val validation = validateAuthFieldsUseCase.validateLogin( + val validation = validateFieldsUseCase.validateLogin( email = _formState.value.email, password = _formState.value.password ) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt index 2397a76..9150254 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt @@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel fun ErrorCollectorScope.MainScreen( modifier: Modifier = Modifier, openResumeDetails: (String) -> Unit, + openCreateResume: () -> Unit, viewModel: MainScreenViewModel = koinViewModel() ) { val typography = MaterialTheme.typography @@ -53,7 +54,7 @@ fun ErrorCollectorScope.MainScreen( Column( modifier = modifier .fillMaxSize() - .padding(horizontal = 20.dp), + .padding(horizontal = Paddings.large), horizontalAlignment = Alignment.CenterHorizontally ) { TopLogo() @@ -78,9 +79,13 @@ fun ErrorCollectorScope.MainScreen( color = colorScheme.onBackground ) - BigButton(onClick = { - TODO() - }, buttonText = "Создать резюме", isLoading = false) + BigButton( + onClick = { + TODO() + }, + buttonText = "Создать резюме", + isLoading = false + ) } else if (items.loadState.hasError) { Text( modifier = Modifier @@ -123,7 +128,7 @@ fun ErrorCollectorScope.MainScreen( .align(Alignment.BottomCenter) .padding(bottom = Paddings.medium), onClick = { - Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show() + openCreateResume() }, text = "Добавить резюме" ) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt index 7df1569..b934b05 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt @@ -2,7 +2,6 @@ package com.prodhack.moscow2025.presentation.screens.main import androidx.paging.map import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase -import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo import com.prodhack.moscow2025.presentation.dataModels.mapToBaseUIInfo import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import kotlinx.coroutines.flow.map diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreen.kt index cbd3d48..eb7123d 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreen.kt @@ -1,14 +1,9 @@ package com.prodhack.moscow2025.presentation.screens.profile -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.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -17,16 +12,12 @@ 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -40,24 +31,19 @@ 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.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 androidx.compose.ui.unit.sp import com.prodhack.moscow2025.R -import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.presentation.components.standart.BigButton -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.theme.Paddings import com.prodhack.moscow2025.presentation.theme.Shapes import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope -import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreenViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreenViewModel.kt index dd3822c..845333c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreenViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/profile/ProfileScreenViewModel.kt @@ -6,8 +6,6 @@ import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.provider.MediaStore -import android.util.Log -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -16,13 +14,13 @@ import coil.ImageLoader import coil.request.ImageRequest import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider import com.prodhack.moscow2025.domain.interfaces.GalleryRepository +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.domain.models.UpdateUserData -import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase import com.prodhack.moscow2025.domain.usecase.auth.GetUserUseCase import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase -import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase -import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI import com.prodhack.moscow2025.presentation.utils.UIState @@ -39,7 +37,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel -data class ProfileState( +data class ProfileFormState( val email: String = "", val firstName: String = "", val lastName: String = "", @@ -51,7 +49,7 @@ data class ProfileState( if (this === other) return true if (javaClass != other?.javaClass) return false - other as ProfileState + other as ProfileFormState if (email != other.email) return false if (firstName != other.firstName) return false @@ -72,21 +70,19 @@ data class ProfileState( result = 31 * result + errors.hashCode() return result } - - } @KoinViewModel class ProfileScreenViewModel( private val getUserUseCase: GetUserUseCase, private val updateUserUseCase: UpdateUserUseCase, - private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, + private val validateFieldsUseCase: ValidateFieldsUseCase, private val logOutUseCase: LogOutUseCase, private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase, galleryRepository: GalleryRepository ) : BaseViewModel() { - private val _formStateProfile = MutableStateFlow(ProfileState()) - val formStateFillProfile: StateFlow = _formStateProfile + private val _formStateProfile = MutableStateFlow(ProfileFormState()) + val formStateFillProfile: StateFlow = _formStateProfile private val _profileState = MutableUIStateFlow() val profileState: StateFlow> = _profileState @@ -203,7 +199,7 @@ class ProfileScreenViewModel( fun submit() { viewModelScope.launch { val pattern = chosenPattern.value - val validation = validateAuthFieldsUseCase.validateProfile( + val validation = validateFieldsUseCase.validateProfile( chosenPattern = pattern?.mapToDomain(), firstName = _formStateProfile.value.firstName, lastName = _formStateProfile.value.lastName, 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 deda99a..e624b7f 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 @@ -37,7 +37,7 @@ 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.domain.models.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 diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt index 6f78027..b7f39dd 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt @@ -1,10 +1,10 @@ package com.prodhack.moscow2025.presentation.screens.register import androidx.lifecycle.viewModelScope +import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.domain.models.RegisterData -import com.prodhack.moscow2025.domain.usecase.auth.AuthField import com.prodhack.moscow2025.domain.usecase.auth.RegisterUserUseCase -import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,7 +23,7 @@ data class RegisterFormState( @KoinViewModel class RegisterViewModel( private val registerUserUseCase: RegisterUserUseCase, - private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase + private val validateFieldsUseCase: ValidateFieldsUseCase ) : BaseViewModel() { private val _formStateSignUp = MutableStateFlow(RegisterFormState()) @@ -58,8 +58,7 @@ class RegisterViewModel( fun submit() { viewModelScope.launch { - - val validation = validateAuthFieldsUseCase.validateSignUp( + val validation = validateFieldsUseCase.validateSignUp( email = _formStateSignUp.value.email, password = _formStateSignUp.value.password, confirmPassword = _formStateSignUp.value.confirmPassword @@ -79,32 +78,6 @@ class RegisterViewModel( ) ) result.collectRequest(_registerState) - -// val validation = validateAuthFieldsUseCase.validateRegister( -// firstName = _formStateSignUp.value.firstName, -// lastName = _formStateSignUp.value.lastName, -// email = _formStateSignUp.value.email, -// password = _formStateSignUp.value.password, -// confirmPassword = _formStateSignUp.value.confirmPassword, -// phone = _formStateSignUp.value.ph -// ) -// -// if (!validation.isValid) { -// _formStateSignUp.update { it.copy(errors = validation.errors) } -// return@launch -// } -// -// _registerState.emit(UIState.Loading()) -// -// val result = registerUserUseCase( -// RegisterData( -// firstName = _formStateSignUp.value.firstName, -// secondName = _formStateSignUp.value.lastName, -// email = _formStateSignUp.value.email, -// password = _formStateSignUp.value.password -// ) -// ) -// result.collectRequest(_registerState) } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt index 0f56b58..14633b6 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -sealed class UIState { +sealed class UIState() { class Idle : UIState() class Loading : UIState() class Error(val error: NetworkError) : UIState() @@ -43,6 +43,9 @@ sealed class UIState { val isSuccess: Boolean get() = this is Success + + val isLoading: Boolean + get() = this is Loading } interface ErrorCallbacks { diff --git a/app/src/main/res/drawable/ic_remove.xml b/app/src/main/res/drawable/ic_remove.xml new file mode 100644 index 0000000..5731006 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47517b0..0428ff7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,7 @@ googleServicesGMC = "4.4.4" crashlytics = "3.0.6" foundation = "1.9.4" kotzilla = "1.4.0" +runtime = "1.9.5" [libraries] @@ -116,6 +117,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } kotzilla-sdk = { group = "io.kotzilla", name = "kotzilla-sdk", version.ref = "kotzilla" } +androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } [plugins]