feat: new FAB

This commit is contained in:
MaximOksiuta
2025-11-23 01:28:17 +03:00
parent ff3cde0a06
commit 4c26f28e35
7 changed files with 221 additions and 184 deletions
@@ -1,53 +0,0 @@
package com.prodhack.moscow2025.presentation.components.standart
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.prodhack.moscow2025.R
@Composable
fun TTFloatingActionButton(
modifier: Modifier,
onClick: () -> Unit,
text: String
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
ExtendedFloatingActionButton(
modifier = modifier,
onClick = {
onClick()
},
shape = RoundedCornerShape(10.dp),
containerColor = colorScheme.tertiaryContainer,
contentColor = colorScheme.onTertiaryContainer,
elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 5.dp)
) {
Row {
Text(
text = text,
style = typography.titleMedium,
fontSize = 16.sp
)
Spacer(Modifier.width(10.dp))
Icon(
painter = painterResource(R.drawable.add_square_outline),
contentDescription = null,
modifier = Modifier.size(22.dp)
)
}
}
}
@@ -118,7 +118,11 @@ fun TTasksNavHost(
}
composable(AppDestination.ResumeCreation.route) {
CreateResumeScreen({ navController.popBackStack() })
CreateResumeScreen({ navController.popBackStack() }, openResumeDetails = { id ->
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
putString(AppDestination.ResumeDetails.ARG_ID, id)
})
})
}
}
}
@@ -42,14 +42,16 @@ import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithD
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch
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.toReadableText
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
import org.koin.androidx.compose.koinViewModel
@Composable
fun CreateResumeScreen(
fun ErrorCollectorScope.CreateResumeScreen(
goBack: () -> Unit,
openResumeDetails: (String) -> Unit,
viewModel: CreateResumeViewModel = koinViewModel()
) {
val colorScheme = MaterialTheme.colorScheme
@@ -122,7 +124,11 @@ fun CreateResumeScreen(
error = formState.value.errors[ResumeField.Experience],
dropdownItems = viewModel.experienceOptions,
dropDownItem = {
Text(text = it.toReadableText(), style = typography.labelLarge, fontSize = 16.sp)
Text(
text = it.toReadableText(),
style = typography.labelLarge,
fontSize = 16.sp
)
},
onDropdownItemSelected = viewModel::onExperienceSelect
)
@@ -187,99 +193,117 @@ fun CreateResumeScreen(
Spacer(modifier = Modifier.height(Paddings.large))
SectionCard(title = "Подробнее о вашем опыте работы:") {
formState.value.workExperience.forEachIndexed { index, workExp ->
WorkExperienceForm(
index = index,
workExp = workExp,
errors = formState.value.errors,
onPlaceChange = { viewModel.changeWorkExperiencePlace(index, it) },
onDescriptionChange = { viewModel.changeWorkExperienceDescription(index, it) },
onDurationChange = { viewModel.changeWorkExperienceMonthDuration(index, it) },
onRemove = { viewModel.removeExperience(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.workExperience.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewExperience,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
SectionCard(title = "Подробнее о вашем опыте работы:") {
formState.value.workExperience.forEachIndexed { index, workExp ->
WorkExperienceForm(
index = index,
workExp = workExp,
errors = formState.value.errors,
onPlaceChange = { viewModel.changeWorkExperiencePlace(index, it) },
onDescriptionChange = {
viewModel.changeWorkExperienceDescription(
index,
it
)
},
onDurationChange = {
viewModel.changeWorkExperienceMonthDuration(
index,
it
)
},
onRemove = { viewModel.removeExperience(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.workExperience.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewExperience,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
)
}
Spacer(modifier = Modifier.height(Paddings.large))
SectionCard(title = "Ваше образование:") {
formState.value.education.forEachIndexed { index, education ->
EducationForm(
index = index,
education = education,
errors = formState.value.errors,
grades = viewModel.educationGradeOptions,
onPlaceChange = { viewModel.changeEducationPlace(index, it) },
onGradeChange = { viewModel.changeEducationGrade(index, it) },
onSpecializationChange = { viewModel.changeEducationSpecialization(index, it) },
onDescriptionChange = { viewModel.changeEducationDescription(index, it) },
onRemove = { viewModel.removeEducation(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.education.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewEducation,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
SectionCard(title = "Ваше образование:") {
formState.value.education.forEachIndexed { index, education ->
EducationForm(
index = index,
education = education,
errors = formState.value.errors,
grades = viewModel.educationGradeOptions,
onPlaceChange = { viewModel.changeEducationPlace(index, it) },
onGradeChange = { viewModel.changeEducationGrade(index, it) },
onSpecializationChange = {
viewModel.changeEducationSpecialization(
index,
it
)
},
onDescriptionChange = { viewModel.changeEducationDescription(index, it) },
onRemove = { viewModel.removeEducation(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.education.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewEducation,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
)
}
Spacer(modifier = Modifier.height(Paddings.large))
SectionCard(title = "Интересные проекты:") {
formState.value.projects.forEachIndexed { index, project ->
ProjectForm(
index = index,
project = project,
errors = formState.value.errors,
onNameChange = { viewModel.changeProjectName(index, it) },
onDescriptionChange = { viewModel.changeProjectDescription(index, it) },
onRemove = { viewModel.removeProject(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.projects.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewProject,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
SectionCard(title = "Интересные проекты:") {
formState.value.projects.forEachIndexed { index, project ->
ProjectForm(
index = index,
project = project,
errors = formState.value.errors,
onNameChange = { viewModel.changeProjectName(index, it) },
onDescriptionChange = { viewModel.changeProjectDescription(index, it) },
onRemove = { viewModel.removeProject(index) }
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
if (formState.value.projects.isEmpty()) {
EmptyStateText()
Spacer(modifier = Modifier.height(Paddings.medium))
}
AddItemButton(
text = "Добавить",
onClick = viewModel::addNewProject,
containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary
)
}
Spacer(modifier = Modifier.height(Paddings.large))
val resumeFillState = viewModel.resumeFillState.collectAsStateWithCallbacks {
openResumeDetails(it)
}
BigButton(
onClick = viewModel::submit,
buttonText = "Узнать свою ЗП",
isLoading = viewModel.resumeFillState.collectAsState().value.isLoading
buttonText = "Узнать свою зарплату",
isLoading = resumeFillState.value.isLoading
)
Spacer(modifier = Modifier.height(Paddings.large))
}
}
}
@@ -1,9 +1,7 @@
package com.prodhack.moscow2025.presentation.screens.main
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -16,21 +14,25 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.prodhack.moscow2025.R
import com.prodhack.moscow2025.presentation.components.standart.BigButton
import com.prodhack.moscow2025.presentation.components.standart.TTFloatingActionButton
import com.prodhack.moscow2025.presentation.components.standart.TopLogo
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
import com.prodhack.moscow2025.presentation.theme.Paddings
@@ -45,31 +47,64 @@ fun ErrorCollectorScope.MainScreen(
openResumeDetails: (String) -> Unit,
openCreateResume: () -> Unit,
viewModel: MainScreenViewModel = koinViewModel()
) {
Scaffold(
modifier = modifier,
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
openCreateResume()
},
icon = {
Icon(
painter = painterResource(R.drawable.ic_plus),
"Extended floating action button."
)
},
text = { Text(text = "Добавить резюме") },
)
}, floatingActionButtonPosition = FabPosition.Center
) {
val items = viewModel.resumeList.collectAsLazyPagingItems()
MainScreenContent(
items = items,
openCreateResume = openCreateResume,
openResumeDetails = openResumeDetails
)
}
}
@Composable
private fun MainScreenContent(
modifier: Modifier = Modifier,
items: LazyPagingItems<UIResumeBaseInfo>,
openCreateResume: () -> Unit,
openResumeDetails: (String) -> Unit
) {
val typography = MaterialTheme.typography
val colorScheme = MaterialTheme.colorScheme
val shapes = MaterialTheme.shapes
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = Paddings.large),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopLogo()
Spacer(modifier = Modifier.height(Paddings.medium))
Text(
text = "Ваши резюме",
style = typography.titleLarge,
fontSize = 32.sp,
color = colorScheme.onBackground
)
Box {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = Paddings.large),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopLogo()
Spacer(modifier = Modifier.height(Paddings.medium))
Text(
text = "Ваши резюме",
style = typography.titleLarge,
fontSize = 32.sp,
color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(Paddings.large))
val items = viewModel.resumeList.collectAsLazyPagingItems()
Spacer(modifier = Modifier.height(Paddings.large))
PullToRefreshBox(items.loadState.refresh is LoadState.Loading, onRefresh = {
items.refresh()
}) {
if (items.itemCount == 0 && items.loadState.append.endOfPaginationReached) {
Text(
text = "Здесь пока ничего нет",
@@ -82,9 +117,7 @@ fun ErrorCollectorScope.MainScreen(
Spacer(modifier = Modifier.height(Paddings.large))
BigButton(
onClick = {
TODO()
},
onClick = openCreateResume,
buttonText = "Создать резюме",
isLoading = false
)
@@ -100,6 +133,14 @@ fun ErrorCollectorScope.MainScreen(
fontSize = 24.sp,
color = colorScheme.onError
)
Spacer(modifier = Modifier.height(Paddings.large))
BigButton(
onClick = { items.retry() },
buttonText = "Попробовать снова",
isLoading = false
)
} else {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -123,17 +164,6 @@ fun ErrorCollectorScope.MainScreen(
}
}
}
val context = LocalContext.current
TTFloatingActionButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = Paddings.medium),
onClick = {
openCreateResume()
},
text = "Добавить резюме"
)
}
}
@@ -17,8 +17,11 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -33,13 +36,11 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavBackStackEntry
import com.prodhack.moscow2025.R
import com.prodhack.moscow2025.domain.models.Education
import com.prodhack.moscow2025.domain.models.EducationGrades
import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.Project
import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.models.WorkExperience
import com.prodhack.moscow2025.presentation.components.standart.BigButton
import com.prodhack.moscow2025.presentation.components.standart.TBubble
import com.prodhack.moscow2025.presentation.components.standart.TTFloatingActionButton
import com.prodhack.moscow2025.presentation.navigation.AppDestination
import com.prodhack.moscow2025.presentation.theme.Paddings
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
@@ -82,16 +83,21 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
}
}
) { resume ->
ResumeDetailsContent(
resume = resume,
onBack = { navController.popBackStack() },
onEdit = {
Toast.makeText(context, "Редактирование пока недоступно", Toast.LENGTH_SHORT).show()
},
onHistory = {
Toast.makeText(context, "История появится позже", Toast.LENGTH_SHORT).show()
}
)
Scaffold(floatingActionButton = {
ExtendedFloatingActionButton(
onClick = { },
icon = { Icon(painter = painterResource(R.drawable.ic_pen), "Extended floating action button.") },
text = { Text(text = "Редактировать резюме") },
)
}, floatingActionButtonPosition = FabPosition.Center) {
ResumeDetailsContent(
resume = resume,
onBack = { navController.popBackStack() },
onHistory = {
Toast.makeText(context, "История появится позже", Toast.LENGTH_SHORT).show()
}
)
}
}
}
@@ -99,7 +105,6 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
private fun ResumeDetailsContent(
resume: ResumeModel,
onBack: () -> Unit,
onEdit: () -> Unit,
onHistory: () -> Unit
) {
val typography = MaterialTheme.typography
@@ -131,7 +136,14 @@ private fun ResumeDetailsContent(
style = typography.titleLarge,
fontSize = 22.sp
)
Spacer(modifier = Modifier.size(24.dp))
Icon(
modifier = Modifier
.size(24.dp)
.noRippleClickable(onHistory),
painter = painterResource(R.drawable.ic_history),
tint = colorScheme.onBackground,
contentDescription = "open history"
)
}
Column(
+10
View File
@@ -0,0 +1,10 @@
<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="M5.079,5.069C8.874,1.279 15.044,1.319 18.862,5.138C22.682,8.958 22.722,15.131 18.926,18.926C15.13,22.721 8.958,22.682 5.138,18.862C4.063,17.792 3.252,16.487 2.766,15.051C2.281,13.614 2.135,12.085 2.34,10.582C2.367,10.385 2.471,10.206 2.63,10.086C2.788,9.966 2.988,9.913 3.185,9.94C3.382,9.967 3.561,10.071 3.681,10.23C3.802,10.388 3.854,10.588 3.827,10.785C3.653,12.058 3.776,13.355 4.188,14.572C4.599,15.79 5.287,16.895 6.198,17.802C9.443,21.046 14.666,21.065 17.866,17.866C21.065,14.666 21.046,9.443 17.802,6.198C14.559,2.956 9.339,2.935 6.139,6.13L6.887,6.133C6.986,6.133 7.083,6.153 7.174,6.191C7.265,6.23 7.347,6.285 7.416,6.355C7.486,6.425 7.541,6.508 7.578,6.599C7.615,6.69 7.634,6.788 7.634,6.887C7.633,6.985 7.613,7.082 7.575,7.173C7.537,7.264 7.481,7.347 7.411,7.416C7.341,7.485 7.259,7.54 7.167,7.577C7.076,7.615 6.979,7.633 6.88,7.633L4.334,7.621C4.136,7.62 3.947,7.541 3.807,7.401C3.668,7.261 3.589,7.072 3.588,6.874L3.575,4.33C3.575,4.232 3.593,4.134 3.631,4.043C3.668,3.952 3.723,3.869 3.792,3.799C3.861,3.728 3.944,3.673 4.034,3.635C4.125,3.596 4.223,3.577 4.321,3.576C4.42,3.576 4.517,3.594 4.608,3.632C4.7,3.669 4.783,3.724 4.853,3.793C4.923,3.862 4.978,3.945 5.016,4.035C5.055,4.126 5.075,4.224 5.075,4.322L5.079,5.069ZM11.999,7.249C12.198,7.249 12.389,7.328 12.529,7.469C12.67,7.609 12.749,7.8 12.749,7.999V11.689L15.03,13.969C15.102,14.038 15.159,14.121 15.198,14.212C15.238,14.304 15.258,14.402 15.259,14.502C15.26,14.601 15.241,14.7 15.204,14.792C15.166,14.885 15.11,14.968 15.04,15.039C14.969,15.109 14.886,15.165 14.794,15.203C14.701,15.241 14.603,15.259 14.503,15.259C14.404,15.258 14.305,15.237 14.214,15.198C14.122,15.159 14.039,15.102 13.97,15.03L11.25,12.31V8C11.25,7.801 11.329,7.61 11.47,7.47C11.61,7.329 11.801,7.25 12,7.25"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<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="M14.757,2.621C15.635,1.743 16.826,1.25 18.068,1.25C19.31,1.25 20.501,1.743 21.379,2.621C22.257,3.499 22.75,4.69 22.75,5.932C22.75,7.174 22.257,8.365 21.379,9.243L11.893,18.729C11.351,19.271 11.033,19.589 10.677,19.866C10.258,20.194 9.808,20.472 9.327,20.701C8.921,20.894 8.493,21.037 7.767,21.279L4.435,22.389L3.633,22.657C3.314,22.764 2.972,22.779 2.644,22.702C2.317,22.625 2.018,22.458 1.78,22.22C1.542,21.982 1.375,21.683 1.298,21.356C1.221,21.028 1.236,20.686 1.343,20.367L2.721,16.234C2.963,15.507 3.106,15.079 3.299,14.672C3.528,14.192 3.807,13.742 4.134,13.322C4.41,12.968 4.729,12.649 5.271,12.107L14.757,2.621ZM4.4,20.821L7.241,19.873C8.032,19.609 8.368,19.496 8.681,19.347C9.062,19.164 9.42,18.943 9.754,18.684C10.027,18.47 10.279,18.221 10.869,17.631L18.439,10.061C17.401,9.693 16.459,9.097 15.682,8.317C14.902,7.54 14.307,6.598 13.94,5.56L6.37,13.13C5.78,13.719 5.53,13.97 5.317,14.244C5.057,14.577 4.836,14.935 4.654,15.317C4.505,15.63 4.392,15.966 4.128,16.757L3.18,19.6L4.4,20.821ZM15.155,4.343C15.19,4.518 15.247,4.756 15.344,5.033C15.636,5.87 16.115,6.63 16.744,7.255C17.369,7.884 18.128,8.362 18.965,8.655C19.243,8.752 19.481,8.809 19.656,8.844L20.318,8.182C20.911,7.585 21.243,6.776 21.242,5.934C21.24,5.092 20.905,4.285 20.31,3.69C19.715,3.094 18.908,2.759 18.066,2.758C17.224,2.756 16.415,3.089 15.818,3.682L15.155,4.343Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>