feat: add simple version of form

This commit is contained in:
MaximOksiuta
2025-11-22 14:05:54 +03:00
parent 2c2fb5a4f4
commit 5084dedf90
28 changed files with 661 additions and 161 deletions
+1
View File
@@ -85,6 +85,7 @@ dependencies {
implementation(libs.compose.animation.graphics) implementation(libs.compose.animation.graphics)
implementation(libs.material.icons.extended) implementation(libs.material.icons.extended)
implementation(libs.androidx.foundation) implementation(libs.androidx.foundation)
implementation(libs.androidx.runtime)
androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.tooling)
@@ -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.data.data_providers.local_db.entities.ResumeEntity
import com.prodhack.moscow2025.domain.models.ExperienceType import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.models.ResumeModel
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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 @Serializable
data class ResumeDTO( data class ResumeDTO(
val id: String, 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<String>,
val position: String
)
fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO(
aboutMe = about,
experienceType = experienceType.mapToData(),
keySkills = skills,
position = position
)
@Serializable @Serializable
data class PredictionDTO( data class PredictionDTO(
@SerialName("from_salary") @SerialName("from_salary")
@@ -83,3 +109,14 @@ data class PredictionDTO(
data class ResumeListDTO( data class ResumeListDTO(
val resumes: List<ResumeDTO> val resumes: List<ResumeDTO>
) )
@Serializable
data class ResumeIdDTO(
@SerialName("resume_id")
val resumeId: String
)
@Serializable
data class ResumeSkillDTO(
val name: String
)
@@ -4,12 +4,21 @@ import androidx.paging.map
import com.prodhack.moscow2025.data.base.BaseRepository import com.prodhack.moscow2025.data.base.BaseRepository
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase 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.ResumeListDTO
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository 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.models.ResumeModel
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
import io.ktor.client.request.setBody
import io.ktor.client.request.url import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod 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 kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@@ -37,4 +46,25 @@ class ResumeRepositoryImpl(
}.map { it -> it.resumes.map { it.mapToDB() } } }.map { it -> it.resumes.map { it.mapToDB() } }
} }
).map { it -> it.map { it.mapToDomain() } } ).map { it -> it.map { it.mapToDomain() } }
override suspend fun suggestSkills(query: String): Result<List<String>> =
networkRequest<List<ResumeSkillDTO>> {
method = HttpMethod.Get
url {
path("key_skills")
parameters.append("query", query)
}
}.map { it.map { it.name } }
override suspend fun createResume(resumeForm: ResumeCreationModel): Result<String> =
networkRequest<ResumeIdDTO> {
method = HttpMethod.Post
url {
url("/resume")
}
setBody(ResumeCreateDTO)
contentType(ContentType.Application.Json)
}.map { it.resumeId }
} }
@@ -1,8 +1,12 @@
package com.prodhack.moscow2025.domain.interfaces.resumes 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.models.ResumeModel
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
interface ResumeRepository { interface ResumeRepository {
fun loadResumeList(): RemotePagingWrapper<ResumeModel> fun loadResumeList(): RemotePagingWrapper<ResumeModel>
suspend fun suggestSkills(query: String): Result<List<String>>
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
} }
@@ -9,3 +9,12 @@ data class LoginData(
val email: String, val email: String,
val password: String val password: String
) )
enum class AuthField {
FirstName,
LastName,
Email,
Password,
ConfirmPassword,
Phone
}
@@ -10,6 +10,13 @@ data class ResumeModel(
val recommendedSkills: List<String> val recommendedSkills: List<String>
) )
data class ResumeCreationModel(
val position: String,
val about: String,
val skills: List<String>,
val experienceType: ExperienceType
)
enum class ExperienceType { enum class ExperienceType {
NoExperience, NoExperience,
LessThan1, LessThan1,
@@ -17,3 +24,10 @@ enum class ExperienceType {
Between3And6, Between3And6,
MoreThan6 MoreThan6
} }
enum class ResumeField {
About,
Position,
Experience,
KeySkills
}
@@ -1,55 +1,48 @@
package com.prodhack.moscow2025.domain.usecase.auth package com.prodhack.moscow2025.domain.usecase.auth
import android.util.Log
import android.util.Patterns 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.PhoneNumberPattern
import com.prodhack.moscow2025.domain.models.ResumeField
import com.prodhack.moscow2025.presentation.screens.createResume.UIExperience
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
enum class AuthField { data class ValidationResult<T>(
FirstName, val errors: Map<T, String> = emptyMap()
LastName,
Email,
Password,
ConfirmPassword,
Phone
}
data class ValidationResult(
val errors: Map<AuthField, String> = emptyMap()
) { ) {
val isValid: Boolean val isValid: Boolean
get() = errors.isEmpty() get() = errors.isEmpty()
} }
@Single @Single
class ValidateAuthFieldsUseCase { class ValidateFieldsUseCase {
fun validateProfile( fun validateProfile(
chosenPattern: PhoneNumberPattern?, chosenPattern: PhoneNumberPattern?,
firstName: String, firstName: String,
lastName: String, lastName: String,
email: String, email: String,
phone: String phone: String
): ValidationResult { ): ValidationResult<AuthField> {
val errors = buildMap { val errors = buildMap {
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")
val maxCount = chosenPattern!!.pattern.count { it == '0' } val maxCount = chosenPattern!!.pattern.count { it == '0' }
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put( if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
AuthField.Phone, AuthField.Phone,
"Некорректный номер телефона" "Некорректный номер телефона"
) )
} }
return ValidationResult(errors) return ValidationResult(errors)
} }
fun validateFillProfile( fun validateFillProfile(
chosenPattern: PhoneNumberPattern?, chosenPattern: PhoneNumberPattern?,
firstName: String, firstName: String,
lastName: String, lastName: String,
phone: String phone: String
): ValidationResult { ): ValidationResult<AuthField> {
val errors = buildMap { val errors = buildMap {
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию") if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
@@ -66,7 +59,7 @@ class ValidateAuthFieldsUseCase {
email: String, email: String,
password: String, password: String,
confirmPassword: String confirmPassword: String
): ValidationResult { ): ValidationResult<AuthField> {
val errors = buildMap { val errors = buildMap {
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
validatePassword(password)?.let { put(AuthField.Password, it) } validatePassword(password)?.let { put(AuthField.Password, it) }
@@ -79,6 +72,40 @@ class ValidateAuthFieldsUseCase {
return ValidationResult(errors) return ValidationResult(errors)
} }
fun validateLogin(
email: String,
password: String
): ValidationResult<AuthField> {
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<String>
): ValidationResult<ResumeField> {
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? { fun validatePassword(password: String): String? {
if (password.length < 8) { if (password.length < 8) {
return "Пароль должен быть не менее 8 символов" return "Пароль должен быть не менее 8 символов"
@@ -95,29 +122,6 @@ class ValidateAuthFieldsUseCase {
return null 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 = private fun isEmailValid(email: String): Boolean =
email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches() email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
@@ -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<String> = resumeRepository.createResume(resumeForm)
}
@@ -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<List<String>> =
resumeRepository.suggestSkills(query = query)
// mock
// suspend operator fun invoke(query: String): Result<List<String>> =
// 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))
}
@@ -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"
)
}
}
}
@@ -266,7 +266,7 @@ fun <T> TTTextFieldWithSearch(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: String, value: String,
onValueChange: (String) -> Unit = {}, onValueChange: (String) -> Unit = {},
readOnly: Boolean = true, readOnly: Boolean = false,
label: String, label: String,
error: String? = null, error: String? = null,
singleLine: Boolean = true, singleLine: Boolean = true,
@@ -20,5 +20,7 @@ sealed class AppDestination(val route: String) {
data object ResumeDetails : AppDestination("resume/details") { data object ResumeDetails : AppDestination("resume/details") {
const val ARG_ID = "id" const val ARG_ID = "id"
} }
data object ResumeCreation: AppDestination("resume/creation")
} }
@@ -11,6 +11,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.prodhack.moscow2025.presentation.screens.main.MainScreen import com.prodhack.moscow2025.presentation.screens.main.MainScreen
import com.prodhack.moscow2025.domain.utils.NetworkError 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.fillProfile.FillProfileScreen
import com.prodhack.moscow2025.presentation.screens.login.LoginScreen import com.prodhack.moscow2025.presentation.screens.login.LoginScreen
import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen
@@ -91,11 +92,15 @@ fun TTasksNavHost(
} }
composable(AppDestination.Main.route) { composable(AppDestination.Main.route) {
MainScreen(openResumeDetails = { id -> MainScreen(
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply { openResumeDetails = { id ->
putString(AppDestination.ResumeDetails.ARG_ID, id) navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
}) putString(AppDestination.ResumeDetails.ARG_ID, id)
}) })
}, openCreateResume = {
navController.navigate(AppDestination.ResumeCreation.route)
}
)
} }
composable(AppDestination.Profile.route) composable(AppDestination.Profile.route)
@@ -111,6 +116,10 @@ fun TTasksNavHost(
composable(AppDestination.ResumeDetails.route) { composable(AppDestination.ResumeDetails.route) {
ResumeDetailsScreen(navBackStackEntry = it) ResumeDetailsScreen(navBackStackEntry = it)
} }
composable(AppDestination.ResumeCreation.route) {
CreateResumeScreen()
}
} }
} }
} }
@@ -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
)
}
}
@@ -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<String> = emptySet(),
val errors: Map<ResumeField, String> = 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<ResumeFormState> = _formStateFillResume
private val _resumeFillState = MutableUIStateFlow<String>()
val resumeFillState: StateFlow<UIState<String>> = _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)
}
}
}
@@ -1,35 +1,24 @@
package com.prodhack.moscow2025.presentation.screens.fillProfile package com.prodhack.moscow2025.presentation.screens.fillProfile
import android.util.Log
import androidx.compose.foundation.Image 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width 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.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -43,25 +32,17 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.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.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.prodhack.moscow2025.R 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.BigButton
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField 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.Shapes
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope 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.UIState
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -14,11 +14,11 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository 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.models.UpdateUserData
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase 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.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.UIState
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
@@ -65,7 +65,7 @@ data class FillProfileFormState(
@KoinViewModel @KoinViewModel
class FillProfileViewModel( class FillProfileViewModel(
private val updateUserUseCase: UpdateUserUseCase, private val updateUserUseCase: UpdateUserUseCase,
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, private val validateFieldsUseCase: ValidateFieldsUseCase,
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase, private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
private val galleryRepository: GalleryRepository private val galleryRepository: GalleryRepository
) : BaseViewModel() { ) : BaseViewModel() {
@@ -168,7 +168,7 @@ class FillProfileViewModel(
fun submit() { fun submit() {
viewModelScope.launch { viewModelScope.launch {
val validation = validateAuthFieldsUseCase.validateFillProfile( val validation = validateFieldsUseCase.validateFillProfile(
firstName = _formStateFillProfile.value.firstName, firstName = _formStateFillProfile.value.firstName,
lastName = _formStateFillProfile.value.lastName, lastName = _formStateFillProfile.value.lastName,
phone = _formStateFillProfile.value.phone, phone = _formStateFillProfile.value.phone,
@@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import com.prodhack.moscow2025.R 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.BigButton
import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
import com.prodhack.moscow2025.presentation.components.standart.TTTextField import com.prodhack.moscow2025.presentation.components.standart.TTTextField
@@ -1,10 +1,10 @@
package com.prodhack.moscow2025.presentation.screens.login package com.prodhack.moscow2025.presentation.screens.login
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.prodhack.moscow2025.domain.models.AuthField
import com.prodhack.moscow2025.domain.models.LoginData 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.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.UIState
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -22,7 +22,7 @@ data class LoginFormState(
@KoinViewModel @KoinViewModel
class LoginViewModel( class LoginViewModel(
private val loginUserUseCase: LoginUserUseCase, private val loginUserUseCase: LoginUserUseCase,
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase private val validateFieldsUseCase: ValidateFieldsUseCase
) : BaseViewModel() { ) : BaseViewModel() {
private val _formState = MutableStateFlow(LoginFormState()) private val _formState = MutableStateFlow(LoginFormState())
@@ -41,7 +41,7 @@ class LoginViewModel(
fun submit() { fun submit() {
viewModelScope.launch { viewModelScope.launch {
val validation = validateAuthFieldsUseCase.validateLogin( val validation = validateFieldsUseCase.validateLogin(
email = _formState.value.email, email = _formState.value.email,
password = _formState.value.password password = _formState.value.password
) )
@@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel
fun ErrorCollectorScope.MainScreen( fun ErrorCollectorScope.MainScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
openResumeDetails: (String) -> Unit, openResumeDetails: (String) -> Unit,
openCreateResume: () -> Unit,
viewModel: MainScreenViewModel = koinViewModel() viewModel: MainScreenViewModel = koinViewModel()
) { ) {
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
@@ -53,7 +54,7 @@ fun ErrorCollectorScope.MainScreen(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 20.dp), .padding(horizontal = Paddings.large),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
TopLogo() TopLogo()
@@ -78,9 +79,13 @@ fun ErrorCollectorScope.MainScreen(
color = colorScheme.onBackground color = colorScheme.onBackground
) )
BigButton(onClick = { BigButton(
TODO() onClick = {
}, buttonText = "Создать резюме", isLoading = false) TODO()
},
buttonText = "Создать резюме",
isLoading = false
)
} else if (items.loadState.hasError) { } else if (items.loadState.hasError) {
Text( Text(
modifier = Modifier modifier = Modifier
@@ -123,7 +128,7 @@ fun ErrorCollectorScope.MainScreen(
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(bottom = Paddings.medium), .padding(bottom = Paddings.medium),
onClick = { onClick = {
Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show() openCreateResume()
}, },
text = "Добавить резюме" text = "Добавить резюме"
) )
@@ -2,7 +2,6 @@ package com.prodhack.moscow2025.presentation.screens.main
import androidx.paging.map import androidx.paging.map
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase 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.dataModels.mapToBaseUIInfo
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -1,14 +1,9 @@
package com.prodhack.moscow2025.presentation.screens.profile 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width 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.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -40,24 +31,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.res.painterResource 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.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.prodhack.moscow2025.R 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.BigButton
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField 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
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope 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.UIState
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar
@@ -6,8 +6,6 @@ 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
@@ -16,13 +14,13 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository 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.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.GetUserUseCase
import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase 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.domain.usecase.GetDefaultPhoneNumberPatternUseCase
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI
import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.UIState
@@ -39,7 +37,7 @@ 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
data class ProfileState( data class ProfileFormState(
val email: String = "", val email: String = "",
val firstName: String = "", val firstName: String = "",
val lastName: String = "", val lastName: String = "",
@@ -51,7 +49,7 @@ data class ProfileState(
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as ProfileState other as ProfileFormState
if (email != other.email) return false if (email != other.email) return false
if (firstName != other.firstName) return false if (firstName != other.firstName) return false
@@ -72,21 +70,19 @@ data class ProfileState(
result = 31 * result + errors.hashCode() result = 31 * result + errors.hashCode()
return result return result
} }
} }
@KoinViewModel @KoinViewModel
class ProfileScreenViewModel( class ProfileScreenViewModel(
private val getUserUseCase: GetUserUseCase, private val getUserUseCase: GetUserUseCase,
private val updateUserUseCase: UpdateUserUseCase, private val updateUserUseCase: UpdateUserUseCase,
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, private val validateFieldsUseCase: ValidateFieldsUseCase,
private val logOutUseCase: LogOutUseCase, private val logOutUseCase: LogOutUseCase,
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase, private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
galleryRepository: GalleryRepository galleryRepository: GalleryRepository
) : BaseViewModel() { ) : BaseViewModel() {
private val _formStateProfile = MutableStateFlow(ProfileState()) private val _formStateProfile = MutableStateFlow(ProfileFormState())
val formStateFillProfile: StateFlow<ProfileState> = _formStateProfile val formStateFillProfile: StateFlow<ProfileFormState> = _formStateProfile
private val _profileState = MutableUIStateFlow<String>() private val _profileState = MutableUIStateFlow<String>()
val profileState: StateFlow<UIState<String>> = _profileState val profileState: StateFlow<UIState<String>> = _profileState
@@ -203,7 +199,7 @@ class ProfileScreenViewModel(
fun submit() { fun submit() {
viewModelScope.launch { viewModelScope.launch {
val pattern = chosenPattern.value val pattern = chosenPattern.value
val validation = validateAuthFieldsUseCase.validateProfile( val validation = validateFieldsUseCase.validateProfile(
chosenPattern = pattern?.mapToDomain(), chosenPattern = pattern?.mapToDomain(),
firstName = _formStateProfile.value.firstName, firstName = _formStateProfile.value.firstName,
lastName = _formStateProfile.value.lastName, lastName = _formStateProfile.value.lastName,
@@ -37,7 +37,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.prodhack.moscow2025.R 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.BigButton
import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
import com.prodhack.moscow2025.presentation.components.standart.TTTextField import com.prodhack.moscow2025.presentation.components.standart.TTTextField
@@ -1,10 +1,10 @@
package com.prodhack.moscow2025.presentation.screens.register package com.prodhack.moscow2025.presentation.screens.register
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.prodhack.moscow2025.domain.models.AuthField
import com.prodhack.moscow2025.domain.models.RegisterData 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.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.UIState
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -23,7 +23,7 @@ data class RegisterFormState(
@KoinViewModel @KoinViewModel
class RegisterViewModel( class RegisterViewModel(
private val registerUserUseCase: RegisterUserUseCase, private val registerUserUseCase: RegisterUserUseCase,
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase private val validateFieldsUseCase: ValidateFieldsUseCase
) : BaseViewModel() { ) : BaseViewModel() {
private val _formStateSignUp = MutableStateFlow(RegisterFormState()) private val _formStateSignUp = MutableStateFlow(RegisterFormState())
@@ -58,8 +58,7 @@ class RegisterViewModel(
fun submit() { fun submit() {
viewModelScope.launch { viewModelScope.launch {
val validation = validateFieldsUseCase.validateSignUp(
val validation = validateAuthFieldsUseCase.validateSignUp(
email = _formStateSignUp.value.email, email = _formStateSignUp.value.email,
password = _formStateSignUp.value.password, password = _formStateSignUp.value.password,
confirmPassword = _formStateSignUp.value.confirmPassword confirmPassword = _formStateSignUp.value.confirmPassword
@@ -79,32 +78,6 @@ class RegisterViewModel(
) )
) )
result.collectRequest(_registerState) 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)
} }
} }
} }
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
sealed class UIState<T> { sealed class UIState<T>() {
class Idle<T> : UIState<T>() class Idle<T> : UIState<T>()
class Loading<T> : UIState<T>() class Loading<T> : UIState<T>()
class Error<T>(val error: NetworkError) : UIState<T>() class Error<T>(val error: NetworkError) : UIState<T>()
@@ -43,6 +43,9 @@ sealed class UIState<T> {
val isSuccess: Boolean val isSuccess: Boolean
get() = this is Success get() = this is Success
val isLoading: Boolean
get() = this is Loading
} }
interface ErrorCallbacks { interface ErrorCallbacks {
+16
View File
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.31,2.25H13.69C13.907,2.25 14.096,2.25 14.274,2.278C14.621,2.334 14.95,2.469 15.234,2.675C15.519,2.88 15.752,3.15 15.914,3.461C15.998,3.621 16.057,3.8 16.126,4.005L16.237,4.34L16.267,4.425C16.358,4.676 16.526,4.892 16.748,5.04C16.97,5.189 17.233,5.262 17.5,5.25H20.5C20.699,5.25 20.89,5.329 21.03,5.47C21.171,5.61 21.25,5.801 21.25,6C21.25,6.199 21.171,6.39 21.03,6.53C20.89,6.671 20.699,6.75 20.5,6.75H3.5C3.301,6.75 3.11,6.671 2.97,6.53C2.829,6.39 2.75,6.199 2.75,6C2.75,5.801 2.829,5.61 2.97,5.47C3.11,5.329 3.301,5.25 3.5,5.25H6.59C6.857,5.244 7.115,5.152 7.326,4.988C7.537,4.824 7.69,4.597 7.763,4.34L7.875,4.005C7.943,3.8 8.002,3.621 8.085,3.461C8.247,3.149 8.48,2.88 8.765,2.675C9.05,2.469 9.379,2.333 9.726,2.278C9.904,2.25 10.093,2.25 10.309,2.25M9.007,5.25C9.076,5.112 9.135,4.969 9.182,4.822L9.282,4.522C9.373,4.249 9.394,4.194 9.415,4.154C9.469,4.05 9.547,3.96 9.642,3.892C9.737,3.823 9.846,3.778 9.962,3.759C10.092,3.747 10.223,3.744 10.354,3.75H13.644C13.932,3.75 13.992,3.752 14.036,3.76C14.152,3.778 14.261,3.824 14.356,3.892C14.451,3.961 14.529,4.05 14.583,4.154C14.604,4.194 14.625,4.249 14.716,4.523L14.816,4.823L14.855,4.935C14.894,5.044 14.94,5.149 14.991,5.25H9.007Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M5.915,8.45C5.902,8.251 5.81,8.066 5.66,7.935C5.511,7.804 5.315,7.738 5.116,7.751C4.918,7.765 4.733,7.856 4.602,8.006C4.471,8.156 4.405,8.351 4.418,8.55L4.882,15.502C4.967,16.784 5.036,17.82 5.198,18.634C5.367,19.479 5.653,20.185 6.245,20.738C6.837,21.291 7.56,21.531 8.415,21.642C9.237,21.75 10.275,21.75 11.561,21.75H12.44C13.725,21.75 14.764,21.75 15.586,21.642C16.44,21.531 17.164,21.292 17.756,20.738C18.347,20.185 18.633,19.478 18.802,18.634C18.964,17.821 19.032,16.784 19.118,15.502L19.582,8.55C19.595,8.351 19.529,8.156 19.398,8.006C19.267,7.856 19.082,7.765 18.883,7.751C18.685,7.738 18.489,7.804 18.34,7.935C18.19,8.066 18.098,8.251 18.085,8.45L17.625,15.35C17.535,16.697 17.471,17.635 17.331,18.34C17.194,19.025 17.004,19.387 16.731,19.643C16.457,19.899 16.083,20.065 15.391,20.155C14.678,20.248 13.738,20.25 12.387,20.25H11.613C10.263,20.25 9.323,20.248 8.609,20.155C7.917,20.065 7.543,19.899 7.269,19.643C6.996,19.387 6.806,19.025 6.669,18.341C6.529,17.635 6.465,16.697 6.375,15.349L5.915,8.45Z"
android:fillColor="#000000"/>
<path
android:pathData="M9.425,10.254C9.623,10.234 9.82,10.294 9.974,10.42C10.128,10.545 10.226,10.727 10.246,10.925L10.746,15.925C10.761,16.12 10.698,16.313 10.573,16.463C10.447,16.613 10.268,16.708 10.073,16.727C9.878,16.747 9.684,16.69 9.531,16.568C9.378,16.446 9.278,16.269 9.254,16.075L8.754,11.075C8.734,10.877 8.794,10.679 8.92,10.526C9.045,10.372 9.227,10.274 9.425,10.254ZM14.575,10.254C14.773,10.274 14.954,10.372 15.08,10.525C15.206,10.679 15.266,10.876 15.246,11.074L14.746,16.074C14.721,16.268 14.622,16.444 14.469,16.566C14.316,16.687 14.122,16.744 13.927,16.725C13.733,16.706 13.554,16.611 13.428,16.462C13.302,16.312 13.24,16.12 13.254,15.925L13.754,10.925C13.774,10.727 13.872,10.546 14.025,10.42C14.179,10.294 14.377,10.234 14.575,10.254Z"
android:fillColor="#000000"/>
</vector>
+2
View File
@@ -39,6 +39,7 @@ googleServicesGMC = "4.4.4"
crashlytics = "3.0.6" crashlytics = "3.0.6"
foundation = "1.9.4" foundation = "1.9.4"
kotzilla = "1.4.0" kotzilla = "1.4.0"
runtime = "1.9.5"
[libraries] [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" } compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
kotzilla-sdk = { group = "io.kotzilla", name = "kotzilla-sdk", version.ref = "kotzilla" } kotzilla-sdk = { group = "io.kotzilla", name = "kotzilla-sdk", version.ref = "kotzilla" }
androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
[plugins] [plugins]