You've already forked RekomenciMobile
feat: add simple version of form
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<String>,
|
||||
val position: String
|
||||
)
|
||||
|
||||
fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO(
|
||||
aboutMe = about,
|
||||
experienceType = experienceType.mapToData(),
|
||||
keySkills = skills,
|
||||
position = position
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PredictionDTO(
|
||||
@SerialName("from_salary")
|
||||
@@ -83,3 +109,14 @@ data class PredictionDTO(
|
||||
data class ResumeListDTO(
|
||||
val resumes: List<ResumeDTO>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResumeIdDTO(
|
||||
@SerialName("resume_id")
|
||||
val resumeId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResumeSkillDTO(
|
||||
val name: String
|
||||
)
|
||||
+30
@@ -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<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 }
|
||||
}
|
||||
+4
@@ -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<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 password: String
|
||||
)
|
||||
|
||||
enum class AuthField {
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
Password,
|
||||
ConfirmPassword,
|
||||
Phone
|
||||
}
|
||||
@@ -10,6 +10,13 @@ data class ResumeModel(
|
||||
val recommendedSkills: List<String>
|
||||
)
|
||||
|
||||
data class ResumeCreationModel(
|
||||
val position: String,
|
||||
val about: String,
|
||||
val skills: List<String>,
|
||||
val experienceType: ExperienceType
|
||||
)
|
||||
|
||||
enum class ExperienceType {
|
||||
NoExperience,
|
||||
LessThan1,
|
||||
@@ -17,3 +24,10 @@ enum class ExperienceType {
|
||||
Between3And6,
|
||||
MoreThan6
|
||||
}
|
||||
|
||||
enum class ResumeField {
|
||||
About,
|
||||
Position,
|
||||
Experience,
|
||||
KeySkills
|
||||
}
|
||||
+44
-40
@@ -1,36 +1,29 @@
|
||||
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<AuthField, String> = emptyMap()
|
||||
data class ValidationResult<T>(
|
||||
val errors: Map<T, String> = emptyMap()
|
||||
) {
|
||||
val isValid: Boolean
|
||||
get() = errors.isEmpty()
|
||||
}
|
||||
|
||||
@Single
|
||||
class ValidateAuthFieldsUseCase {
|
||||
class ValidateFieldsUseCase {
|
||||
fun validateProfile(
|
||||
chosenPattern: PhoneNumberPattern?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
phone: String
|
||||
): ValidationResult {
|
||||
): ValidationResult<AuthField> {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
@@ -49,7 +42,7 @@ class ValidateAuthFieldsUseCase {
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phone: String
|
||||
): ValidationResult {
|
||||
): ValidationResult<AuthField> {
|
||||
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<AuthField> {
|
||||
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<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? {
|
||||
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()
|
||||
|
||||
+12
@@ -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)
|
||||
}
|
||||
+28
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -266,7 +266,7 @@ fun <T> TTTextFieldWithSearch(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit = {},
|
||||
readOnly: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
label: String,
|
||||
error: String? = null,
|
||||
singleLine: Boolean = true,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+162
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+149
@@ -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
-20
@@ -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
|
||||
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
BigButton(
|
||||
onClick = {
|
||||
TODO()
|
||||
}, buttonText = "Создать резюме", isLoading = false)
|
||||
},
|
||||
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 = "Добавить резюме"
|
||||
)
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+1
-15
@@ -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
|
||||
|
||||
+9
-13
@@ -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<ProfileState> = _formStateProfile
|
||||
private val _formStateProfile = MutableStateFlow(ProfileFormState())
|
||||
val formStateFillProfile: StateFlow<ProfileFormState> = _formStateProfile
|
||||
|
||||
private val _profileState = MutableUIStateFlow<String>()
|
||||
val profileState: StateFlow<UIState<String>> = _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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+4
-31
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
sealed class UIState<T> {
|
||||
sealed class UIState<T>() {
|
||||
class Idle<T> : UIState<T>()
|
||||
class Loading<T> : UIState<T>()
|
||||
class Error<T>(val error: NetworkError) : UIState<T>()
|
||||
@@ -43,6 +43,9 @@ sealed class UIState<T> {
|
||||
|
||||
val isSuccess: Boolean
|
||||
get() = this is Success
|
||||
|
||||
val isLoading: Boolean
|
||||
get() = this is Loading
|
||||
}
|
||||
|
||||
interface ErrorCallbacks {
|
||||
|
||||
@@ -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>
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user