From cd737ef957765e05f55e33affd38a3283e4f8b6a Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:01:36 +0300 Subject: [PATCH 001/126] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Interview=5FTrainer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/gradle.xml | 9 ++- .../interview-trainer/api/build.gradle.kts | 50 +++++++++++++ .../api/src/main/AndroidManifest.xml | 4 + .../interview-trainer/impl/build.gradle.kts | 75 +++++++++++++++++++ .../impl/src/main/AndroidManifest.xml | 4 + settings.gradle.kts | 3 + 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 feature/interview-trainer/api/build.gradle.kts create mode 100644 feature/interview-trainer/api/src/main/AndroidManifest.xml create mode 100644 feature/interview-trainer/impl/build.gradle.kts create mode 100644 feature/interview-trainer/impl/src/main/AndroidManifest.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index d6d139c0..5e39de75 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -36,18 +36,21 @@ diff --git a/feature/interview-trainer/api/build.gradle.kts b/feature/interview-trainer/api/build.gradle.kts new file mode 100644 index 00000000..3de6e572 --- /dev/null +++ b/feature/interview-trainer/api/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.yeahub.interview_trainer.api" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } +} + +dependencies { + implementation(project(":core:navigation-api")) + + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.android) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/feature/interview-trainer/api/src/main/AndroidManifest.xml b/feature/interview-trainer/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/interview-trainer/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts new file mode 100644 index 00000000..80db0ed2 --- /dev/null +++ b/feature/interview-trainer/impl/build.gradle.kts @@ -0,0 +1,75 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.yeahub.interview_trainer.impl" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } +} + +dependencies { + //Все нужные модули + implementation(project(":core:navigation-api")) + implementation(project(":core:network-api")) + implementation(project(":feature:interview-trainer:api")) + implementation(project(":core:utils")) + implementation(project(":core:ui")) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.android) + + implementation(libs.compose.shimmer) + + //KOIN + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) + + // Navigation Compose + implementation(libs.androidx.navigation.compose) + + // Timber + implementation(libs.timber) + implementation(libs.androidx.ui.tooling.preview.android) + + testImplementation(libs.junit.jupiter) + testImplementation(platform(libs.junit.bom)) + testRuntimeOnly(libs.junit.platform.launcher) + implementation(libs.androidx.junit.ktx) + testImplementation(libs.mockk) +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/AndroidManifest.xml b/feature/interview-trainer/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 616cc767..facd22a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -58,3 +58,6 @@ include(":feature:questions-or-collections") include(":feature:questions-or-collections:api") include(":feature:questions-or-collections:impl") +include(":feature:interview-trainer") +include(":feature:interview-trainer:api") +include(":feature:interview-trainer:impl") From 95a86311f64800f5dfeafd7709e74f1609144ad3 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:05 +0300 Subject: [PATCH 002/126] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D1=8C=D1=8E=20=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0=20-=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../createQuiz/presentation/CreateQuizState.kt | 14 ++++++++++++++ .../presentation/intent/CreateQuizCommand.kt | 10 ++++++++++ .../presentation/intent/CreateQuizEvent.kt | 16 ++++++++++++++++ .../presentation/intent/CreateQuizResult.kt | 10 ++++++++++ 4 files changed, 50 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt new file mode 100644 index 00000000..0da1ea98 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt @@ -0,0 +1,14 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +sealed interface CreateQuizState { + //Изначальный + data object Loading : CreateQuizState + + data class Loaded( + val specializations: List, + val selectedSpecializationId: Long = 11, + val questionsCount: Int = 1, + ) : CreateQuizState + + data class Error(val throwable: Throwable) : CreateQuizState +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt new file mode 100644 index 00000000..6093773c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizCommand { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizCommand + + data object NavigateBack : CreateQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt new file mode 100644 index 00000000..57e2cde0 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt @@ -0,0 +1,16 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizEvent { + data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent + + data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnStartInterviewQuizClick( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizEvent + + data object OnBackClick : CreateQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt new file mode 100644 index 00000000..33e18a3b --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizResult { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizResult + + data object NavigateBack : CreateQuizResult +} \ No newline at end of file From 742d39e1b16908748edcf04a5a65dba9f50fc53a Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:48 +0300 Subject: [PATCH 003/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D0=BC=D1=8B=D1=85=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B0=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 +++++++++ .../impl/src/main/res/drawable/minus_icon.xml | 11 +++++++++++ .../impl/src/main/res/drawable/plus_icon.xml | 11 +++++++++++ .../impl/src/main/res/values/strings.xml | 8 ++++++++ 4 files changed, 39 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/values/strings.xml diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt new file mode 100644 index 00000000..70627fa1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt @@ -0,0 +1,9 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +import androidx.compose.runtime.Immutable + +@Immutable +data class VoSpecialization( + val id: Int, + val title: String +) diff --git a/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml new file mode 100644 index 00000000..cb791905 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml new file mode 100644 index 00000000..61d61034 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml new file mode 100644 index 00000000..4e706c7e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + Подготовка + Собеседование + Выбор специализации + Количество вопросов + Начать + \ No newline at end of file From efed7b2cc245ee023d3f297df384e2cae4390f44 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:03:11 +0300 Subject: [PATCH 004/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=BE=D0=B2=20=D1=84?= =?UTF-8?q?=D0=B8=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/InterviewTrainerApi.kt | 21 +++++++++++++++++++ .../impl/InterviewTrainerFeatureImpl.kt | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt diff --git a/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt new file mode 100644 index 00000000..b98d29ed --- /dev/null +++ b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt @@ -0,0 +1,21 @@ +package ru.yeahub.interview_trainer.api + +import androidx.compose.runtime.Composable + +/** + * API интерфейс для экрана тренажера собеседований. + * + * Демонстрирует: + * - Передачу параметров между экранами + */ +interface InterviewTrainerApi { + /** + * Экран тренажера собеседований. + * + * @param onBackClick Действие при нажатии кнопки "Назад" + */ + @Composable + fun InterviewTrainerScreen( + onBackClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt new file mode 100644 index 00000000..1c9a29f4 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt @@ -0,0 +1,21 @@ +package ru.yeahub.interview_trainer.impl + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import ru.yeahub.navigation_api.FeatureApi +import ru.yeahub.navigation_api.FeatureRoute +import ru.yeahub.navigation_api.NavigationPathManager + +class InterviewTrainerFeatureImpl : FeatureApi { + override fun getFeatureName(): String = FeatureRoute.InterviewTrainerFeature.FEATURE_NAME + + override fun registerGraph( + navGraphBuilder: NavGraphBuilder, + navController: NavHostController, + pathManager: NavigationPathManager, + modifier: Modifier, + ) { + TODO("Not yet implemented") + } +} \ No newline at end of file From 68e79275d96d7d339a7571a93195a9d6bf0dc0a0 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:03:30 +0300 Subject: [PATCH 005/126] =?UTF-8?q?=D0=91=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=20CreateQuizScreen.kt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt new file mode 100644 index 00000000..5ac93810 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -0,0 +1,242 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.ui + +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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 ru.yeahub.core_ui.component.PrimaryButton +import ru.yeahub.core_ui.component.SkillButton +import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.LocalAppTypography +import ru.yeahub.core_ui.theme.colors +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.interview_trainer.impl.R + +private val FIGMA_HORIZONTAL_PADDING = 16.dp +private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp +private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp + +@StaticPreview +@Composable +fun MockScreenUI() { + Scaffold( + containerColor = colors.black10, + topBar = { + TopAppBarWithBottomBorder( + title = TextOrResource.Text("Подготовка"), + onBackClick = { } + ) + } + ) { paddingValues -> + MockCreateQuizScreen( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) + } +} + +@Composable +private fun MockCreateQuizScreen( + modifier: Modifier = Modifier, + titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_screen_main_title), +) { + val context = LocalContext.current + + Column( + modifier = modifier + .padding(horizontal = FIGMA_HORIZONTAL_PADDING), + ) { + Text( + modifier = Modifier + .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING), + style = LocalAppTypography.current.head5, + text = titleText.getString(context), + ) + + MockChooseSpecializationBlock(context = context, selectedSpec = "Android Dev") + + Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING)) + + MockChooseQuestionsCountBlock(context = context) + + Spacer(modifier = Modifier.weight(1f)) + + MockStartQuizButton() + } +} + +@Composable +private fun MockChooseSpecializationBlock( + modifier: Modifier = Modifier, + titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text), + context: Context, + selectedSpec: String? = null, +) { + Column(modifier = modifier) { + Text( + style = LocalAppTypography.current.body3Accent, + text = titleText.getString(context), + ) + + Spacer(modifier = Modifier.padding(vertical = 4.dp)) + + val specs = arrayOf( + "Frontend", + "Backend", + "Data Science", + "Machine Learning", + "Testing", + "iOS Dev", + "Android Dev", + "Game dev" + ) + FlowRow( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + space = 12.dp, + alignment = Alignment.Start + ), + verticalArrangement = Arrangement.spacedBy( + space = 12.dp, + alignment = Alignment.Top + ), + ) { + specs.forEach { spec -> + SkillButton( + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + enabled = true, + activeButton = selectedSpec == spec, + fillButton = true, + text = spec, + onClick = { }, + ) + } + } + } +} + +@Composable +private fun MockChooseQuestionsCountBlock( + modifier: Modifier = Modifier, + titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text), + context: Context, +) { + Column(modifier = modifier) { + Text( + style = LocalAppTypography.current.body3Accent, + text = titleText.getString(context), + ) + + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + + MockQuestionCounter(count = 0) + } +} + +@Composable +private fun MockQuestionCounter( + modifier: Modifier = Modifier, + count: Int, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(12.dp), + color = colors.black50, + ) { + Row( + modifier = Modifier + .padding(vertical = 6.dp, horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(48.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.size(24.dp), + onClick = { } + ) { + Icon( + painter = painterResource(R.drawable.minus_icon), + contentDescription = "Decrease questions count", + tint = colors.black600 + ) + } + + Text( + text = count.toString(), + style = LocalAppTypography.current.body5Accent, + textAlign = TextAlign.Center, + color = colors.black600 + ) + + IconButton( + modifier = Modifier.size(24.dp), + onClick = { } + ) { + Icon( + painter = painterResource(R.drawable.plus_icon), + contentDescription = "Increase questions count", + tint = colors.black600, + ) + } + } + } +} + +@Composable +private fun MockStartQuizButton( + modifier: Modifier = Modifier, +) { + PrimaryButton( + modifier = modifier + .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING) + .height(48.dp) + .fillMaxWidth(), + onClick = { } + ) { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Начать", + style = LocalAppTypography.current.body3Strong, + textAlign = TextAlign.Center, + color = colors.white900 + ) + + Spacer(Modifier.width(8.dp)) + + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(ru.yeahub.ui.R.drawable.arrow_next), + contentDescription = "Start Interview Quiz Session", + tint = colors.white900 + ) + } + } +} \ No newline at end of file From 7acdad3511ec400d089981a23149ea397f86b933 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:51:00 +0300 Subject: [PATCH 006/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0=20=D0=B2?= =?UTF-8?q?=20=D0=BF=D1=83=D1=82=D0=B8=20=D1=84=D0=B8=D1=87=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B1=D1=83=D0=B4=D1=83=D1=89=D0=B5=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt index 446c60e6..bccd38fb 100644 --- a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt +++ b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt @@ -62,4 +62,8 @@ object FeatureRoute { object PublicCollectionsFeature { const val FEATURE_NAME = "public_collections" } + + object InterviewTrainerFeature { + const val FEATURE_NAME = "interview_trainer" + } } \ No newline at end of file From a49296bda3b2be6e4da54c19f5c04ae6e8619147 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 10 Jan 2026 15:16:36 +0300 Subject: [PATCH 007/126] =?UTF-8?q?ANDR-5:=20VoSpecialization=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B8=20Loaded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/CreateQuizState.kt | 10 +++++++++- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 --------- 2 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt index 0da1ea98..4ad0365f 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt @@ -1,5 +1,7 @@ package ru.yeahub.interview_trainer.impl.createQuiz.presentation +import androidx.compose.runtime.Immutable + sealed interface CreateQuizState { //Изначальный data object Loading : CreateQuizState @@ -8,7 +10,13 @@ sealed interface CreateQuizState { val specializations: List, val selectedSpecializationId: Long = 11, val questionsCount: Int = 1, - ) : CreateQuizState + ) : CreateQuizState { + @Immutable + data class VoSpecialization( + val id: Int, + val title: String + ) + } data class Error(val throwable: Throwable) : CreateQuizState } \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt deleted file mode 100644 index 70627fa1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation - -import androidx.compose.runtime.Immutable - -@Immutable -data class VoSpecialization( - val id: Int, - val title: String -) From 0a2cec3718bcdcafded97bad9d33e06d2930af5f Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 10 Jan 2026 15:43:56 +0300 Subject: [PATCH 008/126] =?UTF-8?q?ANDR-5:=20SkillButton.kt=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD.=20=D0=A3=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B2=D0=BA=D0=B0=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8/=D0=BD=D0=B0=D0=B6=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=BD=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/yeahub/core_ui/component/SkillButton.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt index 8294368a..83cb838b 100644 --- a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt +++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt @@ -138,12 +138,9 @@ fun DefaultButton( val onSurfaceClick: () -> Unit = { if (fillButton) { - newContainerColor = if (newContainerColor == defaultColor) { - purple - } else { - defaultColor - } - newContentColor = if (newContentColor == black) { + showBorder = !showBorder + + newContentColor = if (showBorder) { defaultColor } else { black @@ -163,6 +160,7 @@ fun DefaultButton( } val border = when { showBorder && !fillButton && !buttonWithoutBackground -> activeBorder() + fillButton && enabled && activeButton -> activeBorder() fillButton && enabled -> null buttonWithoutBackground -> null else -> defaultsBorder() @@ -171,7 +169,7 @@ fun DefaultButton( if (newContentColor == black && buttonWithoutBackground && activeButton) { purple } else if (newContentColor == black && fillButton && activeButton) { - defaultColor + black } else { newContentColor } @@ -182,7 +180,7 @@ fun DefaultButton( enabled = enabled, shape = shape, color = if (activeButton && fillButton) { - purple + defaultColor } else { if (buttonWithoutBackground) Color.Transparent else newContainerColor }, From 54d700bf22c18c1c036d105ee8fe9bf5530cc2e5 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sun, 11 Jan 2026 17:26:46 +0300 Subject: [PATCH 009/126] =?UTF-8?q?ANDR-5:=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=83=D1=80=D1=81=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D1=8B=D1=85=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/src/main/res/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index 4e706c7e..ba38acde 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -5,4 +5,12 @@ Выбор специализации Количество вопросов Начать + + Ошибка + Нет подключения к сети + Время ожидания ответа истекло + УПС! + Назад + Не удалось загрузить данные + Что‑то пошло не так \ No newline at end of file From f8b2d96dd680d2a585658b350bf9dc1ee70aeaac Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sun, 11 Jan 2026 19:08:37 +0300 Subject: [PATCH 010/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=BE=D1=82=D1=81=D1=82=D1=83=D0=BF=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 109 ++++++++++++++++-- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index 5ac93810..b940420a 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -2,6 +2,7 @@ package ru.yeahub.interview_trainer.impl.createQuiz.ui import android.content.Context import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues @@ -25,7 +26,10 @@ 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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.ErrorScreen import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.SkillButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder @@ -34,28 +38,49 @@ import ru.yeahub.core_ui.theme.LocalAppTypography import ru.yeahub.core_ui.theme.colors import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState private val FIGMA_HORIZONTAL_PADDING = 16.dp private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp -@StaticPreview + @Composable -fun MockScreenUI() { +fun MockScreenUI( + state: CreateQuizState, + headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) +) { Scaffold( containerColor = colors.black10, topBar = { TopAppBarWithBottomBorder( - title = TextOrResource.Text("Подготовка"), + title = headerText, onBackClick = { } ) } ) { paddingValues -> - MockCreateQuizScreen( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) + Box( + modifier = Modifier.padding(paddingValues) + ) { + when (state) { + CreateQuizState.Loading -> {} + + is CreateQuizState.Error -> { + ErrorScreen( + error = state.throwable.localizedMessage, + errorText = TextOrResource.Resource(R.string.error_screen_text), + titleText = TextOrResource.Resource(R.string.title_error_screen_text), + backText = TextOrResource.Resource(R.string.back_error_screen_text), + unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), + onBack = { } + ) + } + + is CreateQuizState.Loaded -> { + MockCreateQuizScreen() + } + } + } } } @@ -73,8 +98,8 @@ private fun MockCreateQuizScreen( Text( modifier = Modifier .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING), - style = LocalAppTypography.current.head5, text = titleText.getString(context), + style = LocalAppTypography.current.head5, ) MockChooseSpecializationBlock(context = context, selectedSpec = "Android Dev") @@ -102,7 +127,7 @@ private fun MockChooseSpecializationBlock( text = titleText.getString(context), ) - Spacer(modifier = Modifier.padding(vertical = 4.dp)) + Spacer(modifier = Modifier.height(12.dp)) val specs = arrayOf( "Frontend", @@ -152,7 +177,7 @@ private fun MockChooseQuestionsCountBlock( text = titleText.getString(context), ) - Spacer(modifier = Modifier.padding(vertical = 8.dp)) + Spacer(modifier = Modifier.height(12.dp)) MockQuestionCounter(count = 0) } @@ -239,4 +264,66 @@ private fun MockStartQuizButton( ) } } +} + +data class ScreenParams(val state: CreateQuizState) + +class CreateQuizScreenParamsProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ScreenParams( + CreateQuizState.Loaded( + specializations = listOf( + CreateQuizState.Loaded.VoSpecialization( + id = 11, + title = "Frontend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 1, + title = "Backend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 2, + title = "Data Science" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 3, + title = "Machine Learning" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 4, + title = "Testing" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 5, + title = "iOS Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 21, + title = "Android Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 6, + title = "Game Dev" + ) + ) + ) + ), + ScreenParams( + CreateQuizState.Loading + ), + ScreenParams( + CreateQuizState.Error( + Throwable("Не удалось загрузить данные") + ) + ) + ) +} + +@StaticPreview +@Composable +fun CreateQuizScreenPreview( + @PreviewParameter(CreateQuizScreenParamsProvider::class) + params: ScreenParams, +) { + MockScreenUI(params.state) } \ No newline at end of file From b96cd8abaa54716646c9b2ae11919c548b44d8ae Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sun, 11 Jan 2026 19:08:53 +0300 Subject: [PATCH 011/126] =?UTF-8?q?ANDR-5:=20=D0=AD=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20Cre?= =?UTF-8?q?ateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../createQuiz/ui/CreateQuizScreenLoading.kt | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt new file mode 100644 index 00000000..29138a7f --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt @@ -0,0 +1,131 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.ui + +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 +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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.valentinilk.shimmer.shimmer +import ru.yeahub.core_ui.component.SecondaryButton +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.LocalAppTypography +import ru.yeahub.core_ui.theme.colors +import ru.yeahub.interview_trainer.impl.R + +private val FIGMA_HORIZONTAL_PADDING = 16.dp +private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp +private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp + +@StaticPreview +@Composable +fun CreateQuizLoading( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding(horizontal = FIGMA_HORIZONTAL_PADDING) + ) { + Text( + modifier = Modifier + .padding(vertical = 24.dp), + style = LocalAppTypography.current.head5, + text = stringResource(R.string.create_quiz_screen_main_title), + color = colors.black900 + ) + + PlaceHolderBlock() + + Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING)) + + PlaceHolderBlock() + + Spacer(modifier = Modifier.weight(1f)) + + DisabledStartQuizButton() + } +} + +@Composable +private fun PlaceHolderBlock( + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .shimmer(), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + .background(Color.LightGray, shape = RoundedCornerShape(4.dp)) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(148.dp) + .background(Color.LightGray, shape = RoundedCornerShape(4.dp)) + ) + } +} + +@Composable +private fun DisabledStartQuizButton( + modifier: Modifier = Modifier, +) { + SecondaryButton( + modifier = modifier + .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING) + .height(48.dp) + .fillMaxWidth(), + enabled = false, + onClick = { } + ) { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.create_quiz_start_quiz_button_text), + style = LocalAppTypography.current.body3Strong, + textAlign = TextAlign.Center, + color = colors.white900 + ) + + Spacer(Modifier.width(8.dp)) + + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(ru.yeahub.ui.R.drawable.arrow_next), + contentDescription = "Start Interview Quiz Session", + tint = colors.white900 + ) + } + } +} \ No newline at end of file From 00394a13ee14dad330fcdecc175c23ee5c21e799 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sun, 11 Jan 2026 19:14:34 +0300 Subject: [PATCH 012/126] =?UTF-8?q?ANDR-5:=20=D0=98=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8=20=D0=B2=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B4=D0=BB=D1=8F=20@StaticPreview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index b940420a..2dea20d6 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -44,11 +44,10 @@ private val FIGMA_HORIZONTAL_PADDING = 16.dp private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp - @Composable fun MockScreenUI( state: CreateQuizState, - headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) + headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), ) { Scaffold( containerColor = colors.black10, @@ -63,7 +62,7 @@ fun MockScreenUI( modifier = Modifier.padding(paddingValues) ) { when (state) { - CreateQuizState.Loading -> {} + CreateQuizState.Loading -> CreateQuizLoading() is CreateQuizState.Error -> { ErrorScreen( From 14990289fad3d15e11066f5bf94b2657b0db1e63 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sun, 11 Jan 2026 19:22:50 +0300 Subject: [PATCH 013/126] =?UTF-8?q?ANDR-5:=20=D0=A3=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D0=B5=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interview-trainer/impl/src/main/res/values/strings.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index ba38acde..c72f7ccf 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -6,11 +6,8 @@ Количество вопросов Начать - Ошибка - Нет подключения к сети - Время ожидания ответа истекло УПС! + Что‑то пошло не так Назад Не удалось загрузить данные - Что‑то пошло не так \ No newline at end of file From 2ed93170e9596c110d4b97e1111441ebdb81759f Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 12 Jan 2026 19:03:19 +0300 Subject: [PATCH 014/126] =?UTF-8?q?ANDR-5:=20=D0=A3=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B9=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=81=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B2.=20=D0=92=20StaticPreview=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=B5=D0=BC=20=D1=81=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D1=83=20=D1=81=D1=82=D0=B5=D0=B9=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 91 +++++++++---------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index 2dea20d6..5c907792 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -265,55 +265,48 @@ private fun MockStartQuizButton( } } -data class ScreenParams(val state: CreateQuizState) - -class CreateQuizScreenParamsProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - ScreenParams( - CreateQuizState.Loaded( - specializations = listOf( - CreateQuizState.Loaded.VoSpecialization( - id = 11, - title = "Frontend" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 1, - title = "Backend" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 2, - title = "Data Science" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 3, - title = "Machine Learning" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 4, - title = "Testing" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 5, - title = "iOS Dev" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 21, - title = "Android Dev" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 6, - title = "Game Dev" - ) +class CreateQuizScreenStateParamProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + CreateQuizState.Loaded( + specializations = listOf( + CreateQuizState.Loaded.VoSpecialization( + id = 11, + title = "Frontend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 1, + title = "Backend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 2, + title = "Data Science" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 3, + title = "Machine Learning" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 4, + title = "Testing" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 5, + title = "iOS Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 21, + title = "Android Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 6, + title = "Game Dev" ) ) ), - ScreenParams( - CreateQuizState.Loading - ), - ScreenParams( - CreateQuizState.Error( - Throwable("Не удалось загрузить данные") - ) + CreateQuizState.Loading, + + CreateQuizState.Error( + Throwable("Не удалось загрузить данные") ) ) } @@ -321,8 +314,8 @@ class CreateQuizScreenParamsProvider : PreviewParameterProvider { @StaticPreview @Composable fun CreateQuizScreenPreview( - @PreviewParameter(CreateQuizScreenParamsProvider::class) - params: ScreenParams, + @PreviewParameter(CreateQuizScreenStateParamProvider::class) + state: CreateQuizState, ) { - MockScreenUI(params.state) + MockScreenUI(state) } \ No newline at end of file From 1a8ac426e097de676a336dfd0b981f3c220085d3 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Fri, 16 Jan 2026 14:50:42 +0300 Subject: [PATCH 015/126] =?UTF-8?q?ANDR-5:=20=D0=A3=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20Mock=20=D1=83=20@Composable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index 5c907792..b236f48a 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -45,7 +45,7 @@ private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp @Composable -fun MockScreenUI( +fun ScreenUI( state: CreateQuizState, headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), ) { @@ -75,16 +75,14 @@ fun MockScreenUI( ) } - is CreateQuizState.Loaded -> { - MockCreateQuizScreen() - } + is CreateQuizState.Loaded -> CreateQuizScreen() } } } } @Composable -private fun MockCreateQuizScreen( +private fun CreateQuizScreen( modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_screen_main_title), ) { @@ -101,20 +99,20 @@ private fun MockCreateQuizScreen( style = LocalAppTypography.current.head5, ) - MockChooseSpecializationBlock(context = context, selectedSpec = "Android Dev") + ChooseSpecializationBlock(context = context, selectedSpec = "Android Dev") Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING)) - MockChooseQuestionsCountBlock(context = context) + ChooseQuestionsCountBlock(context = context) Spacer(modifier = Modifier.weight(1f)) - MockStartQuizButton() + StartQuizButton() } } @Composable -private fun MockChooseSpecializationBlock( +private fun ChooseSpecializationBlock( modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text), context: Context, @@ -165,7 +163,7 @@ private fun MockChooseSpecializationBlock( } @Composable -private fun MockChooseQuestionsCountBlock( +private fun ChooseQuestionsCountBlock( modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text), context: Context, @@ -178,12 +176,12 @@ private fun MockChooseQuestionsCountBlock( Spacer(modifier = Modifier.height(12.dp)) - MockQuestionCounter(count = 0) + QuestionCounter(count = 0) } } @Composable -private fun MockQuestionCounter( +private fun QuestionCounter( modifier: Modifier = Modifier, count: Int, ) { @@ -231,7 +229,7 @@ private fun MockQuestionCounter( } @Composable -private fun MockStartQuizButton( +private fun StartQuizButton( modifier: Modifier = Modifier, ) { PrimaryButton( @@ -317,5 +315,5 @@ fun CreateQuizScreenPreview( @PreviewParameter(CreateQuizScreenStateParamProvider::class) state: CreateQuizState, ) { - MockScreenUI(state) + ScreenUI(state) } \ No newline at end of file From 8eb8ae0acfc91d7367be8863bd849ff7190fa596 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Fri, 16 Jan 2026 18:15:36 +0300 Subject: [PATCH 016/126] =?UTF-8?q?ANDR-5:=20id=20=D1=81=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=20Long=20=D0=BF=D0=BE=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=B8=20=D1=81=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/CreateQuizState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt index 4ad0365f..d85b4a51 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt @@ -13,8 +13,8 @@ sealed interface CreateQuizState { ) : CreateQuizState { @Immutable data class VoSpecialization( - val id: Int, - val title: String + val id: Long, + val title: String, ) } From 3e69cfac9509881a41c92f39f31e7e259191e040 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:02 +0300 Subject: [PATCH 017/126] =?UTF-8?q?ANDR-5:=20Command'=D1=8B=20=D0=B8=20Res?= =?UTF-8?q?ult'=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20intents.=20=D0=A1?= =?UTF-8?q?=D0=B0=D0=BC=D0=B0=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../createQuiz/presentation/{intent => }/CreateQuizCommand.kt | 2 +- .../createQuiz/presentation/{intent => }/CreateQuizEvent.kt | 0 .../createQuiz/presentation/{intent => }/CreateQuizResult.kt | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/{intent => }/CreateQuizCommand.kt (97%) rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/{intent => }/CreateQuizEvent.kt (100%) rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/{intent => }/CreateQuizResult.kt (97%) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt similarity index 97% rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt index 6093773c..22d52a61 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt @@ -1,4 +1,4 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent +package ru.yeahub.interview_trainer.impl.createQuiz.presentation sealed interface CreateQuizCommand { data class NavigateToInterviewQuizScreen( diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt similarity index 100% rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt similarity index 97% rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt index 33e18a3b..ed6bede3 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt @@ -1,4 +1,4 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent +package ru.yeahub.interview_trainer.impl.createQuiz.presentation sealed interface CreateQuizResult { data class NavigateToInterviewQuizScreen( From 1fef1a45d29229f8ec0ba4f7f14aadbd31e26972 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:20 +0300 Subject: [PATCH 018/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20=D1=8E=D0=B7=D0=BA=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt new file mode 100644 index 00000000..7c2b7f8e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +interface GetSpecializationListUseCase { + + suspend operator fun invoke(): DomainSpecializationListResponse +} \ No newline at end of file From 635231a41ac4ca264a64e2e0111cc81415290408 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:45 +0300 Subject: [PATCH 019/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20Domain=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/DomainSpecialization.kt | 6 ++++++ .../createQuiz/domain/DomainSpecializationListResponse.kt | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt new file mode 100644 index 00000000..79312ae1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +data class DomainSpecialization( + val id: Long, + val title: String, +) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt new file mode 100644 index 00000000..d5f3062d --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +data class DomainSpecializationListResponse( + val data: List, + val total: Long, +) \ No newline at end of file From 03926407688d87581a0d76534f1d740c6f97306a Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 21:03:06 +0300 Subject: [PATCH 020/126] =?UTF-8?q?ANDR-5:=20=D0=92=D1=8C=D1=8E=D0=9C?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=D0=BA=D0=B0=20=D1=8D=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20CreateQuiz.=20=D0=A1=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BD=D0=B5=20=D0=B4=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CreateQuizViewModel.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt new file mode 100644 index 00000000..88db6d9e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt @@ -0,0 +1,125 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.yeahub.core_utils.BaseViewModel +import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationListUseCase +import ru.yeahub.interview_trainer.impl.createQuiz.ui.specializations + +open class CreateQuizViewModel( + private val getSpecializationListUseCase: GetSpecializationListUseCase, +) : BaseViewModel() { + + //Потом поменять на Loading. Сделано Loaded для dynamic превью. Переменная из экрана CreateQuiz + private val _screenState = MutableStateFlow( + CreateQuizState.Loaded( + specializations + ) + ) + val screenState: StateFlow = _screenState + + private val _commands = MutableSharedFlow() + val commands: SharedFlow = _commands + + init { + viewModelScopeSafe.launch(Dispatchers.IO) { + val specializations = getSpecializationListUseCase() + .data + .map { + CreateQuizState.Loaded.VoSpecialization( + id = it.id, + title = it.title + ) + } + _screenState.update { CreateQuizState.Loaded(specializations) } + } + } + + fun onEvent(event: CreateQuizEvent) { + when (event) { + CreateQuizEvent.OnBackClick -> onBackClick() + + is CreateQuizEvent.OnPlusQuestionClick -> incrementQuestionsCount() + + is CreateQuizEvent.OnMinusQuestionClick -> decrementQuestionsCount() + + is CreateQuizEvent.OnSpecializationClick -> changeChosenSpecialization( + newSpecializationId = event.specializationId + ) + + is CreateQuizEvent.OnStartInterviewQuizClick -> onStartInterviewClick( + specializationId = event.specializationId, + questionCount = event.questionCount + ) + } + } + + private fun onBackClick() { + viewModelScopeSafe.launch(Dispatchers.IO) { + _commands.emit(CreateQuizCommand.NavigateBack) + } + } + + private fun incrementQuestionsCount() { + viewModelScopeSafe.launch(Dispatchers.Default) { + _screenState.update { currentState -> + if (currentState is CreateQuizState.Loaded) { + val incrementedCount = currentState.questionsCount + 1 + val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT) + + currentState.copy(questionsCount = newCount) + } else { + currentState + } + } + } + } + + private fun decrementQuestionsCount() { + viewModelScopeSafe.launch(Dispatchers.Default) { + _screenState.update { currentState -> + if (currentState is CreateQuizState.Loaded) { + val incrementedCount = currentState.questionsCount - 1 + val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT) + + currentState.copy(questionsCount = newCount) + } else { + currentState + } + } + } + } + + private fun changeChosenSpecialization(newSpecializationId: Long) { + viewModelScopeSafe.launch(Dispatchers.Default) { + _screenState.update { currentState -> + if (currentState is CreateQuizState.Loaded) { + currentState.copy(selectedSpecializationId = newSpecializationId) + } else { + currentState + } + } + } + } + + private fun onStartInterviewClick(specializationId: Long, questionCount: Int) { + viewModelScopeSafe.launch(Dispatchers.Default) { + _commands.emit( + CreateQuizCommand.NavigateToInterviewQuizScreen( + specializationId = specializationId, + questionCount = questionCount + ) + ) + } + } + + companion object { + private const val MIN_QUESTIONS_COUNT = 1 + private const val MAX_QUESTIONS_COUNT = 8 + } +} From 04978c4778d0541061e59fa3d8efe67e2f1dee49 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 22:31:31 +0300 Subject: [PATCH 021/126] =?UTF-8?q?ANDR-5:=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BB-=D0=B2?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=83=20=D0=B8=D0=B2=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CreateQuizViewModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt index 88db6d9e..7e3f5eb7 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt @@ -44,9 +44,13 @@ open class CreateQuizViewModel( when (event) { CreateQuizEvent.OnBackClick -> onBackClick() - is CreateQuizEvent.OnPlusQuestionClick -> incrementQuestionsCount() + is CreateQuizEvent.OnPlusQuestionClick -> incrementQuestionsCount( + questionsCount = event.questionsCount + ) - is CreateQuizEvent.OnMinusQuestionClick -> decrementQuestionsCount() + is CreateQuizEvent.OnMinusQuestionClick -> decrementQuestionsCount( + questionsCount = event.questionsCount + ) is CreateQuizEvent.OnSpecializationClick -> changeChosenSpecialization( newSpecializationId = event.specializationId @@ -65,11 +69,11 @@ open class CreateQuizViewModel( } } - private fun incrementQuestionsCount() { + private fun incrementQuestionsCount(questionsCount: Int) { viewModelScopeSafe.launch(Dispatchers.Default) { _screenState.update { currentState -> if (currentState is CreateQuizState.Loaded) { - val incrementedCount = currentState.questionsCount + 1 + val incrementedCount = questionsCount + 1 val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT) currentState.copy(questionsCount = newCount) @@ -80,11 +84,11 @@ open class CreateQuizViewModel( } } - private fun decrementQuestionsCount() { + private fun decrementQuestionsCount(questionsCount: Int) { viewModelScopeSafe.launch(Dispatchers.Default) { _screenState.update { currentState -> if (currentState is CreateQuizState.Loaded) { - val incrementedCount = currentState.questionsCount - 1 + val incrementedCount = questionsCount - 1 val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT) currentState.copy(questionsCount = newCount) From 6d1237f59e3690e90e385deae2df4db27027b4df Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 22:41:20 +0300 Subject: [PATCH 022/126] =?UTF-8?q?ANDR-5:=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=20=D0=BF=D1=83=D1=82=D1=8C=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/CreateQuizEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt index 57e2cde0..c8df9ed8 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt @@ -1,4 +1,4 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent +package ru.yeahub.interview_trainer.impl.createQuiz.presentation sealed interface CreateQuizEvent { data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent From 1eaa176e2ffdbb6158aa423a1910e86dc07355a6 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 22:45:41 +0300 Subject: [PATCH 023/126] =?UTF-8?q?ANDR-5:=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=B4=D0=B0=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0.=20=D0=9D=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BF=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8=D0=B4=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BB=D1=8F?= =?UTF-8?q?=D0=BC=D0=B1=D0=B4=20=D0=B2=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 185 +++++++++++------- 1 file changed, 118 insertions(+), 67 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index b236f48a..a9dcadd0 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -38,6 +38,7 @@ import ru.yeahub.core_ui.theme.LocalAppTypography import ru.yeahub.core_ui.theme.colors import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState private val FIGMA_HORIZONTAL_PADDING = 16.dp @@ -45,8 +46,9 @@ private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp @Composable -fun ScreenUI( +private fun ScreenUI( state: CreateQuizState, + onEvent: (CreateQuizEvent) -> Unit, headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), ) { Scaffold( @@ -54,7 +56,7 @@ fun ScreenUI( topBar = { TopAppBarWithBottomBorder( title = headerText, - onBackClick = { } + onBackClick = { onEvent(CreateQuizEvent.OnBackClick) } ) } ) { paddingValues -> @@ -71,18 +73,46 @@ fun ScreenUI( titleText = TextOrResource.Resource(R.string.title_error_screen_text), backText = TextOrResource.Resource(R.string.back_error_screen_text), unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), - onBack = { } + onBack = { onEvent(CreateQuizEvent.OnBackClick) } ) } - is CreateQuizState.Loaded -> CreateQuizScreen() + is CreateQuizState.Loaded -> BaseCreateQuizScreen( + specializations = state.specializations, + selectedSpecializationId = state.selectedSpecializationId, + questionsCount = state.questionsCount, + onSpecializationClick = { id -> + onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id)) + }, + onPlusQuestionCountClick = { count -> + onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count)) + }, + onMinusQuestionCountClick = { count -> + onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count)) + }, + onStartQuizClick = { specializationId: Long, questionsCount: Int -> + onEvent( + CreateQuizEvent.OnStartInterviewQuizClick( + specializationId = specializationId, + questionCount = questionsCount, + ) + ) + } + ) } } } } @Composable -private fun CreateQuizScreen( +private fun BaseCreateQuizScreen( + specializations: List, + selectedSpecializationId: Long, + questionsCount: Int, + onSpecializationClick: (id: Long) -> Unit, + onPlusQuestionCountClick: (count: Int) -> Unit, + onMinusQuestionCountClick: (count: Int) -> Unit, + onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit, modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_screen_main_title), ) { @@ -99,24 +129,40 @@ private fun CreateQuizScreen( style = LocalAppTypography.current.head5, ) - ChooseSpecializationBlock(context = context, selectedSpec = "Android Dev") + ChooseSpecializationBlock( + context = context, + specializations = specializations, + selectedSpecializationId = selectedSpecializationId, + onSpecializationClick = onSpecializationClick + ) Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING)) - ChooseQuestionsCountBlock(context = context) + ChooseQuestionsCountBlock( + context = context, + questionsCount = questionsCount, + onPlusQuestionCountClick = onPlusQuestionCountClick, + onMinusQuestionCountClick = onMinusQuestionCountClick, + ) Spacer(modifier = Modifier.weight(1f)) - StartQuizButton() + StartQuizButton( + specializationId = selectedSpecializationId, + questionsCount = questionsCount, + onStartQuizClick = onStartQuizClick, + ) } } @Composable private fun ChooseSpecializationBlock( + specializations: List, + selectedSpecializationId: Long, + context: Context, + onSpecializationClick: (Long) -> Unit, modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text), - context: Context, - selectedSpec: String? = null, ) { Column(modifier = modifier) { Text( @@ -126,19 +172,8 @@ private fun ChooseSpecializationBlock( Spacer(modifier = Modifier.height(12.dp)) - val specs = arrayOf( - "Frontend", - "Backend", - "Data Science", - "Machine Learning", - "Testing", - "iOS Dev", - "Android Dev", - "Game dev" - ) FlowRow( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy( space = 12.dp, alignment = Alignment.Start @@ -148,14 +183,14 @@ private fun ChooseSpecializationBlock( alignment = Alignment.Top ), ) { - specs.forEach { spec -> + specializations.forEach { specialization -> SkillButton( contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), enabled = true, - activeButton = selectedSpec == spec, + activeButton = specialization.id == selectedSpecializationId, fillButton = true, - text = spec, - onClick = { }, + text = specialization.title, + onClick = { onSpecializationClick(specialization.id) }, ) } } @@ -164,9 +199,12 @@ private fun ChooseSpecializationBlock( @Composable private fun ChooseQuestionsCountBlock( + context: Context, + questionsCount: Int, + onPlusQuestionCountClick: (count: Int) -> Unit, + onMinusQuestionCountClick: (count: Int) -> Unit, modifier: Modifier = Modifier, titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text), - context: Context, ) { Column(modifier = modifier) { Text( @@ -176,12 +214,18 @@ private fun ChooseQuestionsCountBlock( Spacer(modifier = Modifier.height(12.dp)) - QuestionCounter(count = 0) + QuestionCounter( + count = questionsCount, + onPlusQuestionCountClick = onPlusQuestionCountClick, + onMinusQuestionCountClick = onMinusQuestionCountClick + ) } } @Composable private fun QuestionCounter( + onPlusQuestionCountClick: (count: Int) -> Unit, + onMinusQuestionCountClick: (count: Int) -> Unit, modifier: Modifier = Modifier, count: Int, ) { @@ -198,7 +242,7 @@ private fun QuestionCounter( ) { IconButton( modifier = Modifier.size(24.dp), - onClick = { } + onClick = { onMinusQuestionCountClick(count) } ) { Icon( painter = painterResource(R.drawable.minus_icon), @@ -216,7 +260,7 @@ private fun QuestionCounter( IconButton( modifier = Modifier.size(24.dp), - onClick = { } + onClick = { onPlusQuestionCountClick(count) } ) { Icon( painter = painterResource(R.drawable.plus_icon), @@ -230,6 +274,9 @@ private fun QuestionCounter( @Composable private fun StartQuizButton( + specializationId: Long, + questionsCount: Int, + onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit, modifier: Modifier = Modifier, ) { PrimaryButton( @@ -237,7 +284,7 @@ private fun StartQuizButton( .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING) .height(48.dp) .fillMaxWidth(), - onClick = { } + onClick = { onStartQuizClick(specializationId, questionsCount) } ) { Row( modifier = Modifier.fillMaxSize(), @@ -263,46 +310,47 @@ private fun StartQuizButton( } } +val specializations = listOf( + CreateQuizState.Loaded.VoSpecialization( + id = 11, + title = "Frontend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 1, + title = "Backend" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 2, + title = "Data Science" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 3, + title = "Machine Learning" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 4, + title = "Testing" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 5, + title = "iOS Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 21, + title = "Android Dev" + ), + CreateQuizState.Loaded.VoSpecialization( + id = 6, + title = "Game Dev" + ) +) + class CreateQuizScreenStateParamProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( CreateQuizState.Loaded( - specializations = listOf( - CreateQuizState.Loaded.VoSpecialization( - id = 11, - title = "Frontend" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 1, - title = "Backend" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 2, - title = "Data Science" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 3, - title = "Machine Learning" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 4, - title = "Testing" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 5, - title = "iOS Dev" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 21, - title = "Android Dev" - ), - CreateQuizState.Loaded.VoSpecialization( - id = 6, - title = "Game Dev" - ) - ) + specializations = specializations ), CreateQuizState.Loading, - CreateQuizState.Error( Throwable("Не удалось загрузить данные") ) @@ -315,5 +363,8 @@ fun CreateQuizScreenPreview( @PreviewParameter(CreateQuizScreenStateParamProvider::class) state: CreateQuizState, ) { - ScreenUI(state) + ScreenUI( + state = state, + onEvent = { }, + ) } \ No newline at end of file From 1eb6051fa5ad0c04552f01364a71b9a18d5c32e3 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 22:47:11 +0300 Subject: [PATCH 024/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=20=D0=94=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index a9dcadd0..d6399305 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -21,25 +21,39 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember 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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.delay import ru.yeahub.core_ui.component.ErrorScreen import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.SkillButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.example.dynamicPreview.ProvidePreviewCompositionLocals import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.LocalAppTypography import ru.yeahub.core_ui.theme.colors import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization +import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse +import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationListUseCase import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState +import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel private val FIGMA_HORIZONTAL_PADDING = 16.dp private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp @@ -367,4 +381,64 @@ fun CreateQuizScreenPreview( state = state, onEvent = { }, ) -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Composable +fun DynamicPreviewUI() { + val mockSpecsListUseCase = object : GetSpecializationListUseCase { + override suspend fun invoke(): DomainSpecializationListResponse { + delay(RESPONSE_DELAY) + return DomainSpecializationListResponse( + data = specializations.map { + DomainSpecialization( + it.id, + it.title + ) + }, + total = specializations.count().toLong() + ) + } + } + + val mockViewModel = viewModelCreator { + CreateQuizViewModel(mockSpecsListUseCase) + } + + val state by mockViewModel.screenState.collectAsState() + + LaunchedEffect(Unit) { + //Изначальное кол-во == 1 + mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1)) + // должно быть 2 + mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2)) + // должно быть 3 + mockViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3)) + // должно быть снова 2 + mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(21)) + // С изначально выбранного Frontend Dev должно быть выбрано Android Dev + } + + ProvidePreviewCompositionLocals { + ScreenUI( + state = state, + onEvent = mockViewModel::onEvent + ) + } +} + +typealias ViewModelCreator = () -> ViewModel? + +class ViewModelFactory( + private val viewModelCreator: ViewModelCreator = { null }, +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = viewModelCreator() as T +} + +@Composable +inline fun viewModelCreator(noinline creator: ViewModelCreator): VM = + viewModel(factory = remember { ViewModelFactory(creator) }) + +private const val RESPONSE_DELAY = 2500L \ No newline at end of file From 41f25a7861e14300233bd6df36b6fe821c3597ed Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:27:45 +0300 Subject: [PATCH 025/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20default=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/CreateQuizState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt index d85b4a51..07a2d3c6 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt @@ -8,8 +8,8 @@ sealed interface CreateQuizState { data class Loaded( val specializations: List, - val selectedSpecializationId: Long = 11, - val questionsCount: Int = 1, + val selectedSpecializationId: Long, + val questionsCount: Int, ) : CreateQuizState { @Immutable data class VoSpecialization( From a4396ca79fc35c3c7eaa350d830103580bb2f9ea Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:28:04 +0300 Subject: [PATCH 026/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/DomainSpecialization.kt | 6 ------ .../createQuiz/domain/DomainSpecializationListResponse.kt | 6 ------ .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ------ 3 files changed, 18 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt deleted file mode 100644 index 79312ae1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -data class DomainSpecialization( - val id: Long, - val title: String, -) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt deleted file mode 100644 index d5f3062d..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -data class DomainSpecializationListResponse( - val data: List, - val total: Long, -) \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt deleted file mode 100644 index 7c2b7f8e..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -interface GetSpecializationListUseCase { - - suspend operator fun invoke(): DomainSpecializationListResponse -} \ No newline at end of file From 59e9c7f58e596bfdba009fdc3921a3e57d245009 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:30:06 +0300 Subject: [PATCH 027/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CreateQuizScreenMapper.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt new file mode 100644 index 00000000..47831a56 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt @@ -0,0 +1,14 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +object CreateQuizScreenMapper { + + fun getScreenState( + specializations: List, + selectedSpecializationId: Long, + questionsCount: Int, + ): CreateQuizState = CreateQuizState.Loaded( + specializations = specializations, + selectedSpecializationId = selectedSpecializationId, + questionsCount = questionsCount + ) +} \ No newline at end of file From 0d7468d8e3f1c5485b7b2f5a270118fb26d5fe4c Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:42:54 +0300 Subject: [PATCH 028/126] =?UTF-8?q?ANDR-5:=20=D0=B8=D0=BC=D0=BF=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B5=D1=80=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=84=D0=BB?= =?UTF-8?q?=D0=BE=D1=83=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D1=82=D0=B5=D0=B9=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CreateQuizViewModel.kt | 93 +++++++++---------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt index 7e3f5eb7..cef29f84 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt @@ -4,42 +4,41 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.yeahub.core_utils.BaseViewModel -import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationListUseCase import ru.yeahub.interview_trainer.impl.createQuiz.ui.specializations open class CreateQuizViewModel( - private val getSpecializationListUseCase: GetSpecializationListUseCase, + private val screenMapper: CreateQuizScreenMapper, ) : BaseViewModel() { - //Потом поменять на Loading. Сделано Loaded для dynamic превью. Переменная из экрана CreateQuiz - private val _screenState = MutableStateFlow( - CreateQuizState.Loaded( - specializations + private val userInputState = MutableStateFlow( + UserInput( + selectedSpecializationId = 11, + questionsCount = MIN_QUESTIONS_COUNT ) ) - val screenState: StateFlow = _screenState + + val screenState = userInputState + .map { userInput -> + screenMapper.getScreenState( + specializations = specializations, + selectedSpecializationId = userInput.selectedSpecializationId, + questionsCount = userInput.questionsCount, + ) + }.stateIn( + scope = viewModelScopeSafe, + started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES), + initialValue = CreateQuizState.Loading + ) private val _commands = MutableSharedFlow() val commands: SharedFlow = _commands - init { - viewModelScopeSafe.launch(Dispatchers.IO) { - val specializations = getSpecializationListUseCase() - .data - .map { - CreateQuizState.Loaded.VoSpecialization( - id = it.id, - title = it.title - ) - } - _screenState.update { CreateQuizState.Loaded(specializations) } - } - } - fun onEvent(event: CreateQuizEvent) { when (event) { CreateQuizEvent.OnBackClick -> onBackClick() @@ -70,49 +69,37 @@ open class CreateQuizViewModel( } private fun incrementQuestionsCount(questionsCount: Int) { - viewModelScopeSafe.launch(Dispatchers.Default) { - _screenState.update { currentState -> - if (currentState is CreateQuizState.Loaded) { - val incrementedCount = questionsCount + 1 - val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT) - - currentState.copy(questionsCount = newCount) - } else { - currentState - } + viewModelScopeSafe.launch(Dispatchers.IO) { + userInputState.update { currentInputState -> + val incrementedCount = questionsCount + 1 + val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT) + + currentInputState.copy(questionsCount = newCount) } } } private fun decrementQuestionsCount(questionsCount: Int) { - viewModelScopeSafe.launch(Dispatchers.Default) { - _screenState.update { currentState -> - if (currentState is CreateQuizState.Loaded) { - val incrementedCount = questionsCount - 1 - val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT) - - currentState.copy(questionsCount = newCount) - } else { - currentState - } + viewModelScopeSafe.launch(Dispatchers.IO) { + userInputState.update { currentInputState -> + val incrementedCount = questionsCount - 1 + val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT) + + currentInputState.copy(questionsCount = newCount) } } } private fun changeChosenSpecialization(newSpecializationId: Long) { - viewModelScopeSafe.launch(Dispatchers.Default) { - _screenState.update { currentState -> - if (currentState is CreateQuizState.Loaded) { - currentState.copy(selectedSpecializationId = newSpecializationId) - } else { - currentState - } + viewModelScopeSafe.launch(Dispatchers.IO) { + userInputState.update { currentInputState -> + currentInputState.copy(selectedSpecializationId = newSpecializationId) } } } private fun onStartInterviewClick(specializationId: Long, questionCount: Int) { - viewModelScopeSafe.launch(Dispatchers.Default) { + viewModelScopeSafe.launch(Dispatchers.IO) { _commands.emit( CreateQuizCommand.NavigateToInterviewQuizScreen( specializationId = specializationId, @@ -122,8 +109,14 @@ open class CreateQuizViewModel( } } + private data class UserInput( + val selectedSpecializationId: Long, + val questionsCount: Int, + ) + companion object { private const val MIN_QUESTIONS_COUNT = 1 private const val MAX_QUESTIONS_COUNT = 8 + private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L } } From df6d2ad17261c5a3406a6ef150aa2a88e4690330 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 19:08:05 +0300 Subject: [PATCH 029/126] =?UTF-8?q?ANDR-5:=20=D1=81=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B5?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8=D0=BA=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index d6399305..9cceac1e 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -48,10 +48,8 @@ import ru.yeahub.core_ui.theme.LocalAppTypography import ru.yeahub.core_ui.theme.colors import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R -import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization -import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse -import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationListUseCase import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent +import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreenMapper import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel @@ -362,7 +360,9 @@ val specializations = listOf( class CreateQuizScreenStateParamProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( CreateQuizState.Loaded( - specializations = specializations + specializations = specializations, + selectedSpecializationId = 11, + questionsCount = 1 ), CreateQuizState.Loading, CreateQuizState.Error( @@ -386,34 +386,23 @@ fun CreateQuizScreenPreview( @Preview(showBackground = true) @Composable fun DynamicPreviewUI() { - val mockSpecsListUseCase = object : GetSpecializationListUseCase { - override suspend fun invoke(): DomainSpecializationListResponse { - delay(RESPONSE_DELAY) - return DomainSpecializationListResponse( - data = specializations.map { - DomainSpecialization( - it.id, - it.title - ) - }, - total = specializations.count().toLong() - ) - } - } - val mockViewModel = viewModelCreator { - CreateQuizViewModel(mockSpecsListUseCase) + CreateQuizViewModel(CreateQuizScreenMapper) } val state by mockViewModel.screenState.collectAsState() LaunchedEffect(Unit) { + delay(RESPONSE_DELAY) //Изначальное кол-во == 1 mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1)) + delay(RESPONSE_DELAY) // должно быть 2 mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2)) + delay(RESPONSE_DELAY) // должно быть 3 mockViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3)) + delay(RESPONSE_DELAY) // должно быть снова 2 mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(21)) // С изначально выбранного Frontend Dev должно быть выбрано Android Dev @@ -441,4 +430,4 @@ class ViewModelFactory( inline fun viewModelCreator(noinline creator: ViewModelCreator): VM = viewModel(factory = remember { ViewModelFactory(creator) }) -private const val RESPONSE_DELAY = 2500L \ No newline at end of file +private const val RESPONSE_DELAY = 1500L \ No newline at end of file From 34863b1fe8ff24d4f7f3184a78c389784bc5df37 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 19:08:44 +0300 Subject: [PATCH 030/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20default=20=D0=B4=D0=B8=D1=81=D0=BF=D0=B0=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BF=D1=80=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B2=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/CreateQuizViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt index cef29f84..e39eaa93 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt @@ -28,7 +28,7 @@ open class CreateQuizViewModel( screenMapper.getScreenState( specializations = specializations, selectedSpecializationId = userInput.selectedSpecializationId, - questionsCount = userInput.questionsCount, + questionsCount = userInput.questionsCount ) }.stateIn( scope = viewModelScopeSafe, @@ -69,7 +69,7 @@ open class CreateQuizViewModel( } private fun incrementQuestionsCount(questionsCount: Int) { - viewModelScopeSafe.launch(Dispatchers.IO) { + viewModelScopeSafe.launch { userInputState.update { currentInputState -> val incrementedCount = questionsCount + 1 val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT) @@ -80,7 +80,7 @@ open class CreateQuizViewModel( } private fun decrementQuestionsCount(questionsCount: Int) { - viewModelScopeSafe.launch(Dispatchers.IO) { + viewModelScopeSafe.launch { userInputState.update { currentInputState -> val incrementedCount = questionsCount - 1 val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT) @@ -91,7 +91,7 @@ open class CreateQuizViewModel( } private fun changeChosenSpecialization(newSpecializationId: Long) { - viewModelScopeSafe.launch(Dispatchers.IO) { + viewModelScopeSafe.launch { userInputState.update { currentInputState -> currentInputState.copy(selectedSpecializationId = newSpecializationId) } From 0c5b94bca460f8fad68a06bfc44fddbfa8751182 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 19:16:25 +0300 Subject: [PATCH 031/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D1=8B=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E=20=D0=B2=20=D0=B2=D0=B5=D1=80=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/ui/CreateQuizScreen.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt index 9cceac1e..917a3cc5 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt @@ -61,7 +61,7 @@ private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp private fun ScreenUI( state: CreateQuizState, onEvent: (CreateQuizEvent) -> Unit, - headerText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), + headerText: TextOrResource, ) { Scaffold( containerColor = colors.black10, @@ -109,7 +109,8 @@ private fun ScreenUI( questionCount = questionsCount, ) ) - } + }, + titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title) ) } } @@ -125,8 +126,8 @@ private fun BaseCreateQuizScreen( onPlusQuestionCountClick: (count: Int) -> Unit, onMinusQuestionCountClick: (count: Int) -> Unit, onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit, + titleText: TextOrResource, modifier: Modifier = Modifier, - titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_screen_main_title), ) { val context = LocalContext.current @@ -145,7 +146,8 @@ private fun BaseCreateQuizScreen( context = context, specializations = specializations, selectedSpecializationId = selectedSpecializationId, - onSpecializationClick = onSpecializationClick + onSpecializationClick = onSpecializationClick, + titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text) ) Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING)) @@ -155,6 +157,7 @@ private fun BaseCreateQuizScreen( questionsCount = questionsCount, onPlusQuestionCountClick = onPlusQuestionCountClick, onMinusQuestionCountClick = onMinusQuestionCountClick, + titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text) ) Spacer(modifier = Modifier.weight(1f)) @@ -173,8 +176,8 @@ private fun ChooseSpecializationBlock( selectedSpecializationId: Long, context: Context, onSpecializationClick: (Long) -> Unit, + titleText: TextOrResource, modifier: Modifier = Modifier, - titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text), ) { Column(modifier = modifier) { Text( @@ -215,8 +218,8 @@ private fun ChooseQuestionsCountBlock( questionsCount: Int, onPlusQuestionCountClick: (count: Int) -> Unit, onMinusQuestionCountClick: (count: Int) -> Unit, + titleText: TextOrResource, modifier: Modifier = Modifier, - titleText: TextOrResource = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text), ) { Column(modifier = modifier) { Text( @@ -380,6 +383,7 @@ fun CreateQuizScreenPreview( ScreenUI( state = state, onEvent = { }, + headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), ) } @@ -411,7 +415,8 @@ fun DynamicPreviewUI() { ProvidePreviewCompositionLocals { ScreenUI( state = state, - onEvent = mockViewModel::onEvent + onEvent = mockViewModel::onEvent, + headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) ) } } From 8674faedf703ec25eb7337bf9a55119ff746431a Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 23 Jan 2026 15:20:54 +0300 Subject: [PATCH 032/126] =?UTF-8?q?ANDR-55=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizState.kt | 25 +++ .../interviewQuiz/ui/InterviewQuizScreen.kt | 189 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt new file mode 100644 index 00000000..83314eb8 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -0,0 +1,25 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +import androidx.compose.runtime.Immutable + +sealed interface InterviewQuizState { + + /** Изначальное состояние */ + data object Loading : InterviewQuizState + + @Immutable + data class Loaded( + val questions: List, + val questionsCount: Int + ) : InterviewQuizState{ + + @Immutable + data class VoQuestion( + val id: Long, + val title: String, + val shortAnswer: String + ) + } + + data class Error(val throwable: Throwable) : InterviewQuizState +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt new file mode 100644 index 00000000..6c040dfd --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -0,0 +1,189 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.Theme +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.interview_trainer.impl.R + +@Composable +private fun ScreenUI( + headerText: TextOrResource +) { + + val currentQuestion = 10 + val questionsCount = 45 + + Scaffold( + containerColor = Theme.colors.black10, + topBar = { + TopAppBarWithBottomBorder( + title = headerText, + onBackClick = { TODO("onBackClick don't implemented") } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues).padding(16.dp) + ) { + QuizProgress( + currentQuestion, + questionsCount + ) + + Spacer(Modifier.height(28.dp)) + + QuizCard() + } + + } +} + +@Composable +private fun QuizProgress( + current: Int, + total: Int, + modifier: Modifier = Modifier +) { + + val progress = (current.toFloat() / total).coerceIn(0f, 1f) + + Card( + modifier = modifier.fillMaxWidth().height(80.dp), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Column(modifier = Modifier.padding(16.dp)) { + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(RoundedCornerShape(100)), + color = Theme.colors.purple800, + trackColor = Theme.colors.purple300, + gapSize = (-8).dp, + strokeCap = StrokeCap.Round, + drawStopIndicator = {} + ) + + Spacer(Modifier.height(12.dp)) + + Text( + text = TextOrResource.Text("$current из $total").text, + color = Theme.colors.black500, + modifier = Modifier.align(Alignment.End) + ) + } + } + +} + +@Composable +private fun QuizCard( + modifier: Modifier = Modifier +) { + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Row { + NavQuizButton( + text = TextOrResource.Text("Назад"), + onClick = { TODO() }, + leadingIcon = null + ) + Spacer(Modifier.weight(1f)) + NavQuizButton( + text = TextOrResource.Text("Далее"), + onClick = { TODO() }, + leadingIcon = null + ) + } + } +} + +@Composable +private fun NavQuizButton( + text: TextOrResource, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding +) { + + val context = LocalContext.current + + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + contentPadding = contentPadding + ) { + if (leadingIcon != null) { + //Icon + } + Text( + text = when (text) { + is TextOrResource.Resource -> text.getString(context) + is TextOrResource.Text -> text.text + }, + color = Theme.colors.purple800 + ) + if (trailingIcon != null) { + // Icon + + } + } +} + +@StaticPreview +@Composable +fun InterviewQuizScreen() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + +@Preview(showBackground = true) +@Composable +fun DynamicPreviewUI() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + From 67535fcf7f66c10e92eb2d54db4cbce2547398be Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:05 +0300 Subject: [PATCH 033/126] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D1=8C=D1=8E=20=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0=20-=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ++++++++++ .../presentation/intent/CreateQuizEvent.kt | 16 ++++++++++++++++ .../presentation/intent/CreateQuizResult.kt | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt new file mode 100644 index 00000000..6093773c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizCommand { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizCommand + + data object NavigateBack : CreateQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt new file mode 100644 index 00000000..57e2cde0 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt @@ -0,0 +1,16 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizEvent { + data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent + + data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnStartInterviewQuizClick( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizEvent + + data object OnBackClick : CreateQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt new file mode 100644 index 00000000..33e18a3b --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizResult { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizResult + + data object NavigateBack : CreateQuizResult +} \ No newline at end of file From f3f6b8c440c561d22a998f2019bd66a02442adf8 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:48 +0300 Subject: [PATCH 034/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D0=BC=D1=8B=D1=85=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B0=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt new file mode 100644 index 00000000..70627fa1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt @@ -0,0 +1,9 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +import androidx.compose.runtime.Immutable + +@Immutable +data class VoSpecialization( + val id: Int, + val title: String +) From 87b922dbc56330846b51af213547abbd7c498f93 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 10 Jan 2026 15:16:36 +0300 Subject: [PATCH 035/126] =?UTF-8?q?ANDR-5:=20VoSpecialization=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B8=20Loaded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt deleted file mode 100644 index 70627fa1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation - -import androidx.compose.runtime.Immutable - -@Immutable -data class VoSpecialization( - val id: Int, - val title: String -) From f66fd534145ae14d0119e7fb1b6bfc7e6b111049 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:02 +0300 Subject: [PATCH 036/126] =?UTF-8?q?ANDR-5:=20Command'=D1=8B=20=D0=B8=20Res?= =?UTF-8?q?ult'=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20intents.=20=D0=A1?= =?UTF-8?q?=D0=B0=D0=BC=D0=B0=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ---------- .../presentation/intent/CreateQuizEvent.kt | 16 ---------------- .../presentation/intent/CreateQuizResult.kt | 10 ---------- 3 files changed, 36 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt deleted file mode 100644 index 6093773c..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizCommand { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizCommand - - data object NavigateBack : CreateQuizCommand -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt deleted file mode 100644 index 57e2cde0..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizEvent { - data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent - - data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnStartInterviewQuizClick( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizEvent - - data object OnBackClick : CreateQuizEvent -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt deleted file mode 100644 index 33e18a3b..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizResult { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizResult - - data object NavigateBack : CreateQuizResult -} \ No newline at end of file From 028cf6cf33dda2e2610d8f1d298d3da0d0f53450 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:20 +0300 Subject: [PATCH 037/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20=D1=8E=D0=B7=D0=BA=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt new file mode 100644 index 00000000..7c2b7f8e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +interface GetSpecializationListUseCase { + + suspend operator fun invoke(): DomainSpecializationListResponse +} \ No newline at end of file From 81811e898574549b3f57d394549e7bd5778964f5 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:45 +0300 Subject: [PATCH 038/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20Domain=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/DomainSpecialization.kt | 6 ++++++ .../createQuiz/domain/DomainSpecializationListResponse.kt | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt new file mode 100644 index 00000000..79312ae1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +data class DomainSpecialization( + val id: Long, + val title: String, +) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt new file mode 100644 index 00000000..d5f3062d --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +data class DomainSpecializationListResponse( + val data: List, + val total: Long, +) \ No newline at end of file From 331dc21dda9f95cdf7bb1738ebd6d5808c68e9bc Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:28:04 +0300 Subject: [PATCH 039/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/DomainSpecialization.kt | 6 ------ .../createQuiz/domain/DomainSpecializationListResponse.kt | 6 ------ .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ------ 3 files changed, 18 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt deleted file mode 100644 index 79312ae1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -data class DomainSpecialization( - val id: Long, - val title: String, -) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt deleted file mode 100644 index d5f3062d..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -data class DomainSpecializationListResponse( - val data: List, - val total: Long, -) \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt deleted file mode 100644 index 7c2b7f8e..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -interface GetSpecializationListUseCase { - - suspend operator fun invoke(): DomainSpecializationListResponse -} \ No newline at end of file From f6559d68ded7d441c090c9deeef87458200cdb6f Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 23 Jan 2026 15:20:54 +0300 Subject: [PATCH 040/126] =?UTF-8?q?ANDR-55=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizState.kt | 25 +++ .../interviewQuiz/ui/InterviewQuizScreen.kt | 189 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt new file mode 100644 index 00000000..83314eb8 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -0,0 +1,25 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +import androidx.compose.runtime.Immutable + +sealed interface InterviewQuizState { + + /** Изначальное состояние */ + data object Loading : InterviewQuizState + + @Immutable + data class Loaded( + val questions: List, + val questionsCount: Int + ) : InterviewQuizState{ + + @Immutable + data class VoQuestion( + val id: Long, + val title: String, + val shortAnswer: String + ) + } + + data class Error(val throwable: Throwable) : InterviewQuizState +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt new file mode 100644 index 00000000..6c040dfd --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -0,0 +1,189 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.Theme +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.interview_trainer.impl.R + +@Composable +private fun ScreenUI( + headerText: TextOrResource +) { + + val currentQuestion = 10 + val questionsCount = 45 + + Scaffold( + containerColor = Theme.colors.black10, + topBar = { + TopAppBarWithBottomBorder( + title = headerText, + onBackClick = { TODO("onBackClick don't implemented") } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues).padding(16.dp) + ) { + QuizProgress( + currentQuestion, + questionsCount + ) + + Spacer(Modifier.height(28.dp)) + + QuizCard() + } + + } +} + +@Composable +private fun QuizProgress( + current: Int, + total: Int, + modifier: Modifier = Modifier +) { + + val progress = (current.toFloat() / total).coerceIn(0f, 1f) + + Card( + modifier = modifier.fillMaxWidth().height(80.dp), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Column(modifier = Modifier.padding(16.dp)) { + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(RoundedCornerShape(100)), + color = Theme.colors.purple800, + trackColor = Theme.colors.purple300, + gapSize = (-8).dp, + strokeCap = StrokeCap.Round, + drawStopIndicator = {} + ) + + Spacer(Modifier.height(12.dp)) + + Text( + text = TextOrResource.Text("$current из $total").text, + color = Theme.colors.black500, + modifier = Modifier.align(Alignment.End) + ) + } + } + +} + +@Composable +private fun QuizCard( + modifier: Modifier = Modifier +) { + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Row { + NavQuizButton( + text = TextOrResource.Text("Назад"), + onClick = { TODO() }, + leadingIcon = null + ) + Spacer(Modifier.weight(1f)) + NavQuizButton( + text = TextOrResource.Text("Далее"), + onClick = { TODO() }, + leadingIcon = null + ) + } + } +} + +@Composable +private fun NavQuizButton( + text: TextOrResource, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding +) { + + val context = LocalContext.current + + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + contentPadding = contentPadding + ) { + if (leadingIcon != null) { + //Icon + } + Text( + text = when (text) { + is TextOrResource.Resource -> text.getString(context) + is TextOrResource.Text -> text.text + }, + color = Theme.colors.purple800 + ) + if (trailingIcon != null) { + // Icon + + } + } +} + +@StaticPreview +@Composable +fun InterviewQuizScreen() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + +@Preview(showBackground = true) +@Composable +fun DynamicPreviewUI() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + From 20fa9476c804a260dadc643846a5ef094d847360 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Sat, 24 Jan 2026 19:32:46 +0300 Subject: [PATCH 041/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable-nodpi/arrow_left_alt.xml | 10 ++++++++++ .../src/main/res/drawable-nodpi/arrow_right_alt.xml | 10 ++++++++++ .../res/drawable-nodpi/favorite_outlined_icon.xml | 10 ++++++++++ .../main/res/drawable-nodpi/thumbs_down_icon.xml | 9 +++++++++ .../src/main/res/drawable-nodpi/thumbs_up_icon.xml | 9 +++++++++ .../impl/src/main/res/drawable/ellipse_icon.xml | 9 +++++++++ .../src/main/res/drawable/favorite_filled_icon.xml | 13 +++++++++++++ 7 files changed, 70 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml new file mode 100644 index 00000000..cbc3319d --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml new file mode 100644 index 00000000..8fe08e66 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml new file mode 100644 index 00000000..7d6e597c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml new file mode 100644 index 00000000..dc4be875 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml new file mode 100644 index 00000000..0b07152f --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml new file mode 100644 index 00000000..c0481dc1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml new file mode 100644 index 00000000..7cba0263 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml @@ -0,0 +1,13 @@ + + + + From 7092807607c1c60cc73d8dc174ec14b27c63a4e2 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Sat, 24 Jan 2026 19:34:11 +0300 Subject: [PATCH 042/126] =?UTF-8?q?ANDR-55:=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0,=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B2=20QuizState=20enum=20class,=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizState.kt | 7 +- .../interviewQuiz/ui/InterviewQuizScreen.kt | 436 +++++++++++++++--- .../ui/InterviewQuizScreenLoading.kt | 2 + 3 files changed, 378 insertions(+), 67 deletions(-) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt index 83314eb8..15690ef0 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -10,9 +10,14 @@ sealed interface InterviewQuizState { @Immutable data class Loaded( val questions: List, - val questionsCount: Int + val questionsCount: Int, + val currentQuestion: Int, + val isAnswerVisible: Boolean, + val answers: Map ) : InterviewQuizState{ + enum class QuizAnswer { KNOWN, UNKNOWN, NOTHING } + @Immutable data class VoQuestion( val id: Long, diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 6c040dfd..0f8e0309 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -1,16 +1,27 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.ui +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues 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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -20,24 +31,35 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.ErrorScreen +import ru.yeahub.core_ui.component.SecondaryButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.component.YeahubButtonDefaults import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState + +private val FIGMA_MEDIUM_PADDING = 16.dp +private val FIGMA_LOW_PADDING = 8.dp +private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp + +private val FIGMA_CARD_ELEVATION = 4.dp +private val FIGMA_RADIUS = 12.dp @Composable private fun ScreenUI( - headerText: TextOrResource + headerText: TextOrResource, + state: InterviewQuizState ) { - val currentQuestion = 10 - val questionsCount = 45 - Scaffold( containerColor = Theme.colors.black10, topBar = { @@ -47,19 +69,77 @@ private fun ScreenUI( ) } ) { paddingValues -> - Column( - modifier = Modifier.padding(paddingValues).padding(16.dp) - ) { - QuizProgress( - currentQuestion, - questionsCount - ) + Box(Modifier.padding(paddingValues)) { + when (state) { + is InterviewQuizState.Loaded -> { + val currentQuestion = state.questions[state.currentQuestion] + val currentQuestionNumber = state.questions.indexOf(currentQuestion) + 1 - Spacer(Modifier.height(28.dp)) - - QuizCard() + BaseQuizScreen( + currentQuestionNumber = currentQuestionNumber, + questionsCount = state.questions.count(), + questionText = currentQuestion.title, + shortAnswer = currentQuestion.shortAnswer, + isAnswerVisible = state.isAnswerVisible, + isBackClickable = false, + onBackClick = {}, + isNextClickable = false, + onNextClick = {}, + isFavorite = false, + onFavoriteClick = {} + ) + } + is InterviewQuizState.Error -> { + ErrorScreen( + error = state.throwable.localizedMessage, + errorText = TextOrResource.Resource(R.string.error_screen_text), + titleText = TextOrResource.Resource(R.string.title_error_screen_text), + backText = TextOrResource.Resource(R.string.back_error_screen_text), + unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), + onBack = { TODO() } + ) + } + InterviewQuizState.Loading -> {} + } } + } +} + +@Composable +private fun BaseQuizScreen( + currentQuestionNumber: Int, + questionsCount: Int, + questionText: String, + shortAnswer: String, + isAnswerVisible: Boolean, + isBackClickable: Boolean, + onBackClick: () -> Unit, + isNextClickable: Boolean, + onNextClick: () -> Unit, + isFavorite: Boolean, + onFavoriteClick: () -> Unit +) { + Column( + modifier = Modifier.padding( + start = FIGMA_MEDIUM_PADDING, + end = FIGMA_MEDIUM_PADDING, + top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING + ) + ) { + QuizProgress(currentQuestionNumber, questionsCount) + Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) + QuestionCard( + questionText = questionText, + shortAnswer = shortAnswer, + isAnswerVisible = isAnswerVisible, + isBackClickable = isBackClickable, + onBackClick = onBackClick, + isNextClickable = isNextClickable, + onNextClick = onNextClick, + isFavorite = isFavorite, + onFavoriteClick = onFavoriteClick + ) } } @@ -70,85 +150,197 @@ private fun QuizProgress( modifier: Modifier = Modifier ) { - val progress = (current.toFloat() / total).coerceIn(0f, 1f) - - Card( - modifier = modifier.fillMaxWidth().height(80.dp), - colors = CardDefaults.cardColors( - containerColor = Theme.colors.white900 - ), - shape = RoundedCornerShape(16), - elevation = CardDefaults.cardElevation(8.dp) - ) { + val progress = (current.toFloat() / total) - Column(modifier = Modifier.padding(16.dp)) { + DefaultCard(modifier) { + Column(modifier = Modifier.padding(FIGMA_MEDIUM_PADDING)) { LinearProgressIndicator( progress = { progress }, modifier = Modifier .fillMaxWidth() - .height(10.dp) - .clip(RoundedCornerShape(100)), - color = Theme.colors.purple800, + .height(8.dp) + .clip(RoundedCornerShape(24)), + color = Theme.colors.purple700, trackColor = Theme.colors.purple300, gapSize = (-8).dp, strokeCap = StrokeCap.Round, drawStopIndicator = {} ) - - Spacer(Modifier.height(12.dp)) - + Spacer(Modifier.height(8.dp)) Text( text = TextOrResource.Text("$current из $total").text, color = Theme.colors.black500, + style = Theme.typography.body2Accent, modifier = Modifier.align(Alignment.End) ) } } - } @Composable -private fun QuizCard( - modifier: Modifier = Modifier +private fun QuestionCard( + questionText: String, + shortAnswer: String, + isAnswerVisible: Boolean, + isBackClickable: Boolean, + onBackClick: () -> Unit, + isNextClickable: Boolean, + onNextClick: () -> Unit, + isFavorite: Boolean, + onFavoriteClick: () -> Unit ) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Theme.colors.white900 - ), - shape = RoundedCornerShape(16), - elevation = CardDefaults.cardElevation(8.dp) - ) { + val favoriteIcon: Painter = + painterResource( + if (isFavorite) R.drawable.favorite_filled_icon + else R.drawable.favorite_outlined_icon + ) - Row { - NavQuizButton( - text = TextOrResource.Text("Назад"), - onClick = { TODO() }, - leadingIcon = null + DefaultCard { + Column(Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING)) { + Row(Modifier.fillMaxWidth()) { + NavigationButton( + text = TextOrResource.Text("Назад"), + enabled = isBackClickable, + onClick = onBackClick, + leadingIcon = painterResource(R.drawable.arrow_left_alt) + ) + Spacer(Modifier.weight(1f)) + NavigationButton( + text = TextOrResource.Text("Далее"), + enabled = isNextClickable, + onClick = onNextClick, + trailingIcon = painterResource(R.drawable.arrow_right_alt) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = FIGMA_MEDIUM_PADDING), + verticalAlignment = Alignment.Top + + ) { + Icon( + painter = painterResource(R.drawable.ellipse_icon), + contentDescription = null, + modifier = Modifier.padding(top = 4.dp), + tint = Theme.colors.purple800 + ) + Text( + text = questionText, + modifier = Modifier + .padding(start = FIGMA_LOW_PADDING, end = 12.dp) + .weight(1f), + style = Theme.typography.body3Strong + ) + FilledIconButton( + onClick = onFavoriteClick, + modifier = Modifier.size(48.dp), + shape = RoundedCornerShape(FIGMA_RADIUS), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = Theme.colors.black10 + ) + ) { + Icon( + painter = favoriteIcon, + contentDescription = null, + modifier = Modifier.padding(12.dp), + tint = if (isFavorite) { + Theme.colors.red700 + } else { + Theme.colors.black600 + } + ) + } + } + Text( + text = if (isAnswerVisible) { + "Свернуть ответ" + } else { + "Посмотреть ответ" + }, + modifier = Modifier + .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp, bottom = 12.dp) + .clickable(onClick = { TODO() } ), + style = Theme.typography.body2, + color = Theme.colors.purple700 ) - Spacer(Modifier.weight(1f)) - NavQuizButton( - text = TextOrResource.Text("Далее"), - onClick = { TODO() }, - leadingIcon = null + if (isAnswerVisible) { + Text( + text = shortAnswer, + style = Theme.typography.body3 + ) + } + Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) + + Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) { + QuizAnswerButton( + painter = painterResource(R.drawable.thumbs_down_icon), + text = TextOrResource.Text("Не знаю"), + onClick = { TODO() }, + isSelected = false + ) + Spacer(Modifier.weight(1f)) + QuizAnswerButton( + painter = painterResource(R.drawable.thumbs_up_icon), + text = TextOrResource.Text("Знаю"), + onClick = { TODO() }, + isSelected = false + ) + } + HorizontalDivider( + modifier = Modifier.padding(bottom = FIGMA_MEDIUM_PADDING), + color = Theme.colors.black100 ) + SecondaryButton( + onClick = {}, + modifier = Modifier.width(170.dp).height(48.dp).align(Alignment.End), + colors = YeahubButtonDefaults.secondaryButtonColors( + containerColor = Theme.colors.red100, + contentColor = Theme.colors.red700 + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Завершить", + style = Theme.typography.body3Strong + ) + } + } } } } @Composable -private fun NavQuizButton( +private fun DefaultCard( + modifier: Modifier = Modifier, + content: @Composable (ColumnScope.() -> Unit) +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = Theme.colors.white900), + shape = RoundedCornerShape(FIGMA_RADIUS), + elevation = CardDefaults.cardElevation(FIGMA_CARD_ELEVATION), + content = content + ) +} + +@Composable +private fun NavigationButton( text: TextOrResource, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - leadingIcon: ImageVector? = null, - trailingIcon: ImageVector? = null, - contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding + leadingIcon: Painter? = null, + trailingIcon: Painter? = null, + contentPadding: PaddingValues = PaddingValues() ) { val context = LocalContext.current + val color = if (enabled) Theme.colors.purple700 else Theme.colors.purple300 TextButton( onClick = onClick, @@ -157,33 +349,145 @@ private fun NavQuizButton( contentPadding = contentPadding ) { if (leadingIcon != null) { - //Icon + Icon( + painter = leadingIcon, + contentDescription = null, + tint = color + ) + Spacer(Modifier.width(FIGMA_LOW_PADDING)) } Text( text = when (text) { is TextOrResource.Resource -> text.getString(context) is TextOrResource.Text -> text.text }, - color = Theme.colors.purple800 + color = color, + style = Theme.typography.body3Strong ) if (trailingIcon != null) { - // Icon - + Spacer(Modifier.width(FIGMA_LOW_PADDING)) + Icon( + painter = trailingIcon, + contentDescription = null, + tint = color + ) } } } +@Composable +private fun NavQuizIcon(painter: Painter) { + +} + +@Composable +private fun QuizAnswerButton( + painter: Painter, + text: TextOrResource, + onClick: () -> Unit, + isSelected: Boolean = false +) { + + val context = LocalContext.current + + val contentColor = if (isSelected) { + Theme.colors.purple700 + } else { + Theme.colors.black700 + } + + Button( + onClick = onClick, + modifier = Modifier.width(120.dp).height(48.dp), + shape = RoundedCornerShape(FIGMA_RADIUS), + colors = ButtonDefaults.buttonColors( + containerColor = Theme.colors.black10, + contentColor = contentColor + ), + contentPadding = PaddingValues( + horizontal = 12.dp, + vertical = FIGMA_LOW_PADDING + ) + ) { + Icon( + painter = painter, + contentDescription = null, + modifier = Modifier.padding(end = FIGMA_LOW_PADDING) + ) + Text( + text = when (text) { + is TextOrResource.Text -> text.text + is TextOrResource.Resource -> text.getString(context) + }, + style = Theme.typography.body2 + ) + } +} + +private val questions = listOf( + InterviewQuizState.Loaded.VoQuestion( + id = 12, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 14, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 9, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 5, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ) +) + +private val answers = mapOf( + 12.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 14.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 9.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, + 5.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, +) + +class QuizScreenStateParamProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + InterviewQuizState.Loaded( + questions = questions, + questionsCount = questions.count(), + currentQuestion = 2, + isAnswerVisible = true, + answers = answers + ), + InterviewQuizState.Loading, + InterviewQuizState.Error( + Throwable("Не удалось загрузить данные") + ) + ) +} + @StaticPreview @Composable -fun InterviewQuizScreen() { +fun InterviewQuizScreen( + @PreviewParameter(QuizScreenStateParamProvider::class) + state: InterviewQuizState +) { ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), + state + ) } +/* @Preview(showBackground = true) @Composable fun DynamicPreviewUI() { ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) + ) } - +*/ diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt new file mode 100644 index 00000000..3c9e0d23 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -0,0 +1,2 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.ui + From 0b1a33d66da48de9a2e6e584fba5656fbf13158a Mon Sep 17 00:00:00 2001 From: Deyryl Date: Wed, 28 Jan 2026 16:28:09 +0300 Subject: [PATCH 043/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/src/main/res/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index c72f7ccf..d2385b5f 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -10,4 +10,12 @@ Что‑то пошло не так Назад Не удалось загрузить данные + Проверить результаты + Свернуть ответ + Показать ответ + Не знаю + Знаю + Завершить + Назад + Далее \ No newline at end of file From 252579326dbb43df52710eb99ff0b63649305168 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Thu, 29 Jan 2026 12:04:05 +0300 Subject: [PATCH 044/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20LoadingScreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/InterviewQuizScreenLoading.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt index 3c9e0d23..0f086f3b 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -1,2 +1,53 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.valentinilk.shimmer.shimmer +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.Theme + +@Composable +fun InterviewQuizLoading() { + + Column(Modifier.padding(horizontal = 16.dp).fillMaxSize()) { + PlaceHolderBlock( + Modifier.padding(vertical = 24.dp).fillMaxWidth().height(65.dp) + ) + + PlaceHolderBlock(Modifier.fillMaxWidth().height(320.dp)) + + + } +} + +@Composable +private fun PlaceHolderBlock(modifier: Modifier = Modifier) { + + Card( + modifier = modifier.shimmer(), + colors = CardDefaults.cardColors(containerColor = Theme.colors.white900), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Box(Modifier.fillMaxSize().background(Color.LightGray)) + } +} + +@StaticPreview +@Composable +private fun QuizLoadingStaticPreview() { + + InterviewQuizLoading() +} \ No newline at end of file From 4cd3c41b02f31edc4a4a3d298587c5f22aaccb8b Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:27:44 +0300 Subject: [PATCH 045/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Command,=20Event,=20Scre?= =?UTF-8?q?enMapper,=20State?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizCommand.kt | 8 ++++++ .../presentation/InterviewQuizEvent.kt | 22 ++++++++++++++++ .../presentation/InterviewQuizScreenMapper.kt | 18 +++++++++++++ .../presentation/InterviewQuizState.kt | 25 ++++++++++++++++--- 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt new file mode 100644 index 00000000..fcde5b25 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt @@ -0,0 +1,8 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +sealed interface InterviewQuizCommand { + + data object NavigateToInterviewQuizResultScreen : InterviewQuizCommand + + data object NavigateBack : InterviewQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt new file mode 100644 index 00000000..471d5934 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt @@ -0,0 +1,22 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + + + +sealed interface InterviewQuizEvent { + + data object OnShowResultClick : InterviewQuizEvent + + data object OnKnownAnswerClick : InterviewQuizEvent + + data object OnUnknownAnswerClick : InterviewQuizEvent + + data object OnNextQuestionClick : InterviewQuizEvent + + data object OnPreviousQuestionClick : InterviewQuizEvent + + data object OnFavoriteQuestionClick : InterviewQuizEvent + + data object OnShowHideAnswerClick : InterviewQuizEvent + + data object OnBackClick : InterviewQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt new file mode 100644 index 00000000..3073ccc8 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt @@ -0,0 +1,18 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +object InterviewQuizScreenMapper { + + fun getScreenState( + questions: List, + questionsCount: Int, + currentQuestion: Int, + isAnswerVisible: Boolean, + answers: Map + ): InterviewQuizState = InterviewQuizState.Loaded( + questions = questions, + questionsCount = questionsCount, + currentQuestionIndex = currentQuestion, + isAnswerVisible = isAnswerVisible, + answers = answers + ) +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt index 15690ef0..e001497b 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -11,12 +11,29 @@ sealed interface InterviewQuizState { data class Loaded( val questions: List, val questionsCount: Int, - val currentQuestion: Int, - val isAnswerVisible: Boolean, - val answers: Map + val currentQuestionIndex: Int, + val isAnswerVisible: Boolean = false, + val answers: Map = emptyMap(), ) : InterviewQuizState{ - enum class QuizAnswer { KNOWN, UNKNOWN, NOTHING } + val canGoNext: Boolean + get() { + return if (answers.containsKey(questions[currentQuestionIndex].id) + && currentQuestionIndex != questions.lastIndex) { + true + } else { + false + } + } + + val canGoPrevious: Boolean + get() = currentQuestionIndex > 0 + + val currentAnswer: QuizAnswer + get() = answers[questions[currentQuestionIndex].id] + ?: QuizAnswer.NONE + + enum class QuizAnswer { KNOWN, UNKNOWN, NONE } @Immutable data class VoQuestion( From f715c434ba2e0a0227b101a04b60c8e209e84c0a Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:27:56 +0300 Subject: [PATCH 046/126] =?UTF-8?q?ANDR-55:=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20string=20=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=83=D1=80=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/interview-trainer/impl/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index d2385b5f..26739f27 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -16,6 +16,6 @@ Не знаю Знаю Завершить - Назад + Назад Далее \ No newline at end of file From 32835c5116ab5fac49d00d358e092e9aadeff961 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:28:42 +0300 Subject: [PATCH 047/126] =?UTF-8?q?ANDR-55:=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=BD=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20ViewModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizViewModel.kt | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt new file mode 100644 index 00000000..be0244b7 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt @@ -0,0 +1,170 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.yeahub.core_utils.BaseViewModel + +open class InterviewQuizViewModel( + private val screenMapper: InterviewQuizScreenMapper +) : BaseViewModel() { + + private val _screenState = MutableStateFlow( + InterviewQuizState.Loading + ) + + val screenState = _screenState.stateIn( + scope = viewModelScopeSafe, + started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES), + initialValue = InterviewQuizState.Loading + ) + + private val _commands = MutableSharedFlow() + val commands = _commands.asSharedFlow() + + init { + viewModelScopeSafe.launch { + delay(RESPONSE_DELAY) + initialLoad() + } + } + + fun onEvent(event: InterviewQuizEvent) { + when (event) { + InterviewQuizEvent.OnBackClick -> onBackClick() + InterviewQuizEvent.OnKnownAnswerClick -> onKnownAnswerClick() + InterviewQuizEvent.OnUnknownAnswerClick -> onUnknownAnswerClick() + InterviewQuizEvent.OnShowResultClick -> onShowResultClick() + InterviewQuizEvent.OnFavoriteQuestionClick -> { /* TODO: нет профиля */ } + InterviewQuizEvent.OnNextQuestionClick -> onNextQuestionClick() + InterviewQuizEvent.OnPreviousQuestionClick -> onPreviousQuestionClick() + InterviewQuizEvent.OnShowHideAnswerClick -> onShowHideAnswerClick() + } + } + + private fun onShowHideAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + loaded.copy(isAnswerVisible = !loaded.isAnswerVisible) + } + } + } + + private fun onPreviousQuestionClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + if (loaded.canGoPrevious) { + loaded.copy( + currentQuestionIndex = loaded.currentQuestionIndex - 1, + isAnswerVisible = false + ) + } else { + loaded + } + } + } + } + + private fun onNextQuestionClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + if (loaded.canGoNext) { + loaded.copy( + currentQuestionIndex = loaded.currentQuestionIndex + 1, + isAnswerVisible = false + ) + } else { + loaded + } + } + } + } + + /** Пока нет domain/data - загрузка Мок данных */ + protected open suspend fun initialLoad() { + val questions = previewQuestions() + _screenState.value = InterviewQuizState.Loaded( + questions = questions, + questionsCount = questions.count(), + currentQuestionIndex = 0, + isAnswerVisible = false, + ) + } + + private fun onBackClick() { + viewModelScopeSafe.launch { + _commands.emit(InterviewQuizCommand.NavigateBack) + } + } + + private fun onKnownAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) + ?: return@updateLoaded loaded + loaded.copy( + answers = loaded.answers + + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.KNOWN) + ) + } + } + } + + private fun onUnknownAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) + ?: return@updateLoaded loaded + loaded.copy( + answers = loaded.answers + + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN) + ) + } + } + } + + private fun onShowResultClick() { + viewModelScopeSafe.launch(Dispatchers.IO) { + _commands.emit( + InterviewQuizCommand.NavigateToInterviewQuizResultScreen + ) + } + } + + /** Метод-хелпер для обновления состояния Loaded */ + private inline fun MutableStateFlow.updateLoaded( + transform: (InterviewQuizState.Loaded) -> InterviewQuizState.Loaded + ) { + update { state -> + val loaded = state as? InterviewQuizState.Loaded ?: return@update state + transform(loaded) + } + } + + /** Создание списка вопросов для тестирования превью */ + private fun previewQuestions(): List { + val base1 = InterviewQuizState.Loaded.VoQuestion(0, "Что такое Virtual DOM, и как он работает?", "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений.") + val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") + val questions = mutableListOf() + repeat(10) { index -> + if (index % 2 == 0) { + questions.add(base1.copy(id = index.toLong())) + } else { + questions.add(base2.copy(id = index.toLong())) + } + } + return questions + } + + companion object { + + private const val RESPONSE_DELAY = 2500L + private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L + } +} From 27bd39f426054fd9a8dbb7dbdd22fc2cba12ea71 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:29:57 +0300 Subject: [PATCH 048/126] =?UTF-8?q?ANDR-55:=20=D0=BE=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0.=20=D0=A1=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interviewQuiz/ui/InterviewQuizScreen.kt | 326 ++++++++++++------ 1 file changed, 213 insertions(+), 113 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 0f8e0309..9b3e909a 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -27,6 +27,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -34,10 +40,16 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel import ru.yeahub.core_ui.component.ErrorScreen +import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.SecondaryButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder import ru.yeahub.core_ui.component.YeahubButtonDefaults @@ -45,7 +57,10 @@ import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizEvent +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizScreenMapper import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizViewModel private val FIGMA_MEDIUM_PADDING = 16.dp private val FIGMA_LOW_PADDING = 8.dp @@ -54,10 +69,35 @@ private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp private val FIGMA_CARD_ELEVATION = 4.dp private val FIGMA_RADIUS = 12.dp +private data class QuestionCardState( + val questionText: String, + val shortAnswer: String, + val currentAnswer: InterviewQuizState.Loaded.QuizAnswer, + val isAnswerVisible: Boolean, + val canGoPrevious: Boolean, + val canGoNext: Boolean, + val isLastQuestion: Boolean +) + +private fun InterviewQuizState.Loaded.toQuestionCardState(): QuestionCardState { + val question = questions[currentQuestionIndex] + + return QuestionCardState( + questionText = question.title, + shortAnswer = question.shortAnswer, + currentAnswer = currentAnswer, + isAnswerVisible = isAnswerVisible, + canGoPrevious = canGoPrevious, + canGoNext = canGoNext, + isLastQuestion = currentQuestionIndex == questions.lastIndex + ) +} + @Composable private fun ScreenUI( headerText: TextOrResource, - state: InterviewQuizState + state: InterviewQuizState, + onEvent: (InterviewQuizEvent) -> Unit ) { Scaffold( @@ -71,35 +111,38 @@ private fun ScreenUI( ) { paddingValues -> Box(Modifier.padding(paddingValues)) { when (state) { - is InterviewQuizState.Loaded -> { - val currentQuestion = state.questions[state.currentQuestion] - val currentQuestionNumber = state.questions.indexOf(currentQuestion) + 1 - - BaseQuizScreen( - currentQuestionNumber = currentQuestionNumber, - questionsCount = state.questions.count(), - questionText = currentQuestion.title, - shortAnswer = currentQuestion.shortAnswer, - isAnswerVisible = state.isAnswerVisible, - isBackClickable = false, - onBackClick = {}, - isNextClickable = false, - onNextClick = {}, - isFavorite = false, - onFavoriteClick = {} - ) - } - is InterviewQuizState.Error -> { - ErrorScreen( - error = state.throwable.localizedMessage, - errorText = TextOrResource.Resource(R.string.error_screen_text), - titleText = TextOrResource.Resource(R.string.title_error_screen_text), - backText = TextOrResource.Resource(R.string.back_error_screen_text), - unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), - onBack = { TODO() } - ) - } - InterviewQuizState.Loading -> {} + is InterviewQuizState.Loaded -> BaseQuizScreen( + state = state, + onPreviousClick = { + onEvent(InterviewQuizEvent.OnPreviousQuestionClick) + }, + onNextClick = { + onEvent(InterviewQuizEvent.OnNextQuestionClick) + }, + onUnknownClick = { + onEvent(InterviewQuizEvent.OnUnknownAnswerClick) + }, + onKnownClick = { + onEvent(InterviewQuizEvent.OnKnownAnswerClick) + }, + onShowHideAnswerClick = { + onEvent(InterviewQuizEvent.OnShowHideAnswerClick) + }, + onResultClick = { + onEvent(InterviewQuizEvent.OnShowResultClick) + }, + ) + + is InterviewQuizState.Error -> ErrorScreen( + error = state.throwable.localizedMessage, + errorText = TextOrResource.Resource(R.string.error_screen_text), + titleText = TextOrResource.Resource(R.string.title_error_screen_text), + backText = TextOrResource.Resource(R.string.back_error_screen_text), + unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), + onBack = { TODO() } + ) + + InterviewQuizState.Loading -> InterviewQuizLoading() } } } @@ -107,19 +150,17 @@ private fun ScreenUI( @Composable private fun BaseQuizScreen( - currentQuestionNumber: Int, - questionsCount: Int, - questionText: String, - shortAnswer: String, - isAnswerVisible: Boolean, - isBackClickable: Boolean, - onBackClick: () -> Unit, - isNextClickable: Boolean, + state: InterviewQuizState.Loaded, + onPreviousClick: () -> Unit, onNextClick: () -> Unit, - isFavorite: Boolean, - onFavoriteClick: () -> Unit + onUnknownClick: () -> Unit, + onKnownClick: () -> Unit, + onShowHideAnswerClick: () -> Unit, + onResultClick: () -> Unit ) { + val cardState = state.toQuestionCardState() + Column( modifier = Modifier.padding( start = FIGMA_MEDIUM_PADDING, @@ -127,18 +168,19 @@ private fun BaseQuizScreen( top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING ) ) { - QuizProgress(currentQuestionNumber, questionsCount) + QuizProgress( + current = state.currentQuestionIndex + 1, + total = state.questionsCount, + ) Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) QuestionCard( - questionText = questionText, - shortAnswer = shortAnswer, - isAnswerVisible = isAnswerVisible, - isBackClickable = isBackClickable, - onBackClick = onBackClick, - isNextClickable = isNextClickable, + state = cardState, + onPreviousClick = onPreviousClick, onNextClick = onNextClick, - isFavorite = isFavorite, - onFavoriteClick = onFavoriteClick + onUnknownClick = onUnknownClick, + onKnownClick = onKnownClick, + onShowAnswerClick = onShowHideAnswerClick, + onResultClick = onResultClick, ) } } @@ -179,17 +221,18 @@ private fun QuizProgress( @Composable private fun QuestionCard( - questionText: String, - shortAnswer: String, - isAnswerVisible: Boolean, - isBackClickable: Boolean, - onBackClick: () -> Unit, - isNextClickable: Boolean, + state: QuestionCardState, + onPreviousClick: () -> Unit, onNextClick: () -> Unit, - isFavorite: Boolean, - onFavoriteClick: () -> Unit + onUnknownClick: () -> Unit, + onKnownClick: () -> Unit, + onShowAnswerClick: () -> Unit, + onResultClick: () -> Unit ) { + /* TODO: нет фичи профиля */ + var isFavorite by rememberSaveable { mutableStateOf(false) } + val favoriteIcon: Painter = painterResource( if (isFavorite) R.drawable.favorite_filled_icon @@ -197,18 +240,20 @@ private fun QuestionCard( ) DefaultCard { - Column(Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING)) { + Column(Modifier + .fillMaxWidth() + .padding(FIGMA_MEDIUM_PADDING)) { Row(Modifier.fillMaxWidth()) { NavigationButton( - text = TextOrResource.Text("Назад"), - enabled = isBackClickable, - onClick = onBackClick, + text = TextOrResource.Resource(R.string.quiz_btn_prev), + enabled = state.canGoPrevious, + onClick = onPreviousClick, leadingIcon = painterResource(R.drawable.arrow_left_alt) ) Spacer(Modifier.weight(1f)) NavigationButton( - text = TextOrResource.Text("Далее"), - enabled = isNextClickable, + text = TextOrResource.Resource(R.string.quiz_btn_next), + enabled = state.canGoNext, onClick = onNextClick, trailingIcon = painterResource(R.drawable.arrow_right_alt) ) @@ -227,14 +272,14 @@ private fun QuestionCard( tint = Theme.colors.purple800 ) Text( - text = questionText, + text = state.questionText, modifier = Modifier .padding(start = FIGMA_LOW_PADDING, end = 12.dp) .weight(1f), style = Theme.typography.body3Strong ) FilledIconButton( - onClick = onFavoriteClick, + onClick = { isFavorite = !isFavorite }, modifier = Modifier.size(48.dp), shape = RoundedCornerShape(FIGMA_RADIUS), colors = IconButtonDefaults.filledIconButtonColors( @@ -254,20 +299,21 @@ private fun QuestionCard( } } Text( - text = if (isAnswerVisible) { - "Свернуть ответ" + text = if (state.isAnswerVisible) { + stringResource(R.string.quiz_collapse_answer) } else { - "Посмотреть ответ" + stringResource(R.string.quiz_show_answer) }, modifier = Modifier - .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp, bottom = 12.dp) - .clickable(onClick = { TODO() } ), + .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp) + .clickable(onClick = onShowAnswerClick), style = Theme.typography.body2, color = Theme.colors.purple700 ) - if (isAnswerVisible) { + if (state.isAnswerVisible) { Text( - text = shortAnswer, + text = state.shortAnswer, + modifier = Modifier.padding(top = 12.dp), style = Theme.typography.body3 ) } @@ -276,38 +322,59 @@ private fun QuestionCard( Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) { QuizAnswerButton( painter = painterResource(R.drawable.thumbs_down_icon), - text = TextOrResource.Text("Не знаю"), - onClick = { TODO() }, - isSelected = false + text = TextOrResource.Resource(R.string.quiz_answer_unknown), + onClick = onUnknownClick, + isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN ) Spacer(Modifier.weight(1f)) QuizAnswerButton( painter = painterResource(R.drawable.thumbs_up_icon), - text = TextOrResource.Text("Знаю"), - onClick = { TODO() }, - isSelected = false + text = TextOrResource.Resource(R.string.quiz_answer_known), + onClick = onKnownClick, + isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN ) } HorizontalDivider( modifier = Modifier.padding(bottom = FIGMA_MEDIUM_PADDING), color = Theme.colors.black100 ) - SecondaryButton( - onClick = {}, - modifier = Modifier.width(170.dp).height(48.dp).align(Alignment.End), - colors = YeahubButtonDefaults.secondaryButtonColors( - containerColor = Theme.colors.red100, - contentColor = Theme.colors.red700 - ) - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + if (state.isLastQuestion) { + PrimaryButton( + onClick = onResultClick, + modifier = Modifier.height(48.dp), + ) { - Text( - text = "Завершить", - style = Theme.typography.body3Strong + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.quiz_check_result), + style = Theme.typography.body3Strong + ) + } + } + } else { + SecondaryButton( + onClick = {}, + modifier = Modifier + .width(170.dp) + .height(48.dp) + .align(Alignment.End), + colors = YeahubButtonDefaults.secondaryButtonColors( + containerColor = Theme.colors.red100, + contentColor = Theme.colors.red700 ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.quiz_btn_complete), + style = Theme.typography.body3Strong + ) + } } } } @@ -375,17 +442,12 @@ private fun NavigationButton( } } -@Composable -private fun NavQuizIcon(painter: Painter) { - -} - @Composable private fun QuizAnswerButton( painter: Painter, text: TextOrResource, onClick: () -> Unit, - isSelected: Boolean = false + isSelected: Boolean ) { val context = LocalContext.current @@ -398,7 +460,9 @@ private fun QuizAnswerButton( Button( onClick = onClick, - modifier = Modifier.width(120.dp).height(48.dp), + modifier = Modifier + .width(120.dp) + .height(48.dp), shape = RoundedCornerShape(FIGMA_RADIUS), colors = ButtonDefaults.buttonColors( containerColor = Theme.colors.black10, @@ -426,32 +490,32 @@ private fun QuizAnswerButton( private val questions = listOf( InterviewQuizState.Loaded.VoQuestion( - id = 12, + id = 0, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 14, + id = 1, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 9, + id = 2, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 5, + id = 3, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ) ) private val answers = mapOf( - 12.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, - 14.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, - 9.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, - 5.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, + 0.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.NONE, + 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, ) class QuizScreenStateParamProvider : PreviewParameterProvider { @@ -459,7 +523,21 @@ class QuizScreenStateParamProvider : PreviewParameterProvider { + InterviewQuizViewModel(InterviewQuizScreenMapper) + } + + val state by mockViewModel.screenState.collectAsState() + ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) + headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), + state = state, + onEvent = mockViewModel::onEvent ) } -*/ + +typealias ViewModelCreator = () -> ViewModel? + +class ViewModelFactory( + private val viewModelCreator: ViewModelCreator = { null }, +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = viewModelCreator() as T +} + +@Composable +inline fun viewModelCreator(noinline creator: ViewModelCreator): VM = + viewModel(factory = remember { ViewModelFactory(creator) }) \ No newline at end of file From 137c676d48d38d83f05e9678154c0fdb841192c3 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 16:28:20 +0300 Subject: [PATCH 049/126] =?UTF-8?q?ANDR-55:=20=D1=83=D0=B2=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/interviewQuiz/presentation/InterviewQuizViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt index be0244b7..c3e37c59 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt @@ -150,7 +150,7 @@ open class InterviewQuizViewModel( /** Создание списка вопросов для тестирования превью */ private fun previewQuestions(): List { val base1 = InterviewQuizState.Loaded.VoQuestion(0, "Что такое Virtual DOM, и как он работает?", "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений.") - val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") + val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") val questions = mutableListOf() repeat(10) { index -> if (index % 2 == 0) { From e332c17084fe787dbe7deab93588d65ff4386937 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 16:29:20 +0300 Subject: [PATCH 050/126] =?UTF-8?q?ANDR-55:=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3.=20=D0=9A=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=B0=20=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B0=D1=82=20=D0=BC.=D0=B1.=20=D0=BD=D0=B5=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interviewQuiz/ui/InterviewQuizScreen.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 9b3e909a..a932dbf7 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -13,7 +13,9 @@ 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.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -162,11 +164,13 @@ private fun BaseQuizScreen( val cardState = state.toQuestionCardState() Column( - modifier = Modifier.padding( - start = FIGMA_MEDIUM_PADDING, - end = FIGMA_MEDIUM_PADDING, - top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING - ) + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding( + horizontal = FIGMA_MEDIUM_PADDING, + vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING + ) ) { QuizProgress( current = state.currentQuestionIndex + 1, @@ -342,7 +346,10 @@ private fun QuestionCard( PrimaryButton( onClick = onResultClick, modifier = Modifier.height(48.dp), - + enabled = state.currentAnswer != InterviewQuizState.Loaded.QuizAnswer.NONE, + colors = YeahubButtonDefaults.primaryButtonColors( + disabledContentColor = Theme.colors.black100 + ) ) { Box( modifier = Modifier.fillMaxSize(), @@ -350,7 +357,8 @@ private fun QuestionCard( ) { Text( text = stringResource(R.string.quiz_check_result), - style = Theme.typography.body3Strong + style = Theme.typography.body3Strong, + color = Theme.colors.white900 ) } } @@ -512,10 +520,9 @@ private val questions = listOf( ) private val answers = mapOf( - 0.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, - 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.NONE, - 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN ) class QuizScreenStateParamProvider : PreviewParameterProvider { @@ -538,7 +545,7 @@ class QuizScreenStateParamProvider : PreviewParameterProvider Date: Thu, 8 Jan 2026 21:02:05 +0300 Subject: [PATCH 051/126] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D1=8C=D1=8E=20=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0=20-=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ++++++++++ .../presentation/intent/CreateQuizEvent.kt | 16 ++++++++++++++++ .../presentation/intent/CreateQuizResult.kt | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt new file mode 100644 index 00000000..6093773c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizCommand { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizCommand + + data object NavigateBack : CreateQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt new file mode 100644 index 00000000..57e2cde0 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt @@ -0,0 +1,16 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizEvent { + data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent + + data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnStartInterviewQuizClick( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizEvent + + data object OnBackClick : CreateQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt new file mode 100644 index 00000000..33e18a3b --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizResult { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizResult + + data object NavigateBack : CreateQuizResult +} \ No newline at end of file From 2b6f74127d865601adf61f0d5a9858d3b1084a53 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:48 +0300 Subject: [PATCH 052/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D0=BC=D1=8B=D1=85=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B0=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt new file mode 100644 index 00000000..70627fa1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt @@ -0,0 +1,9 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +import androidx.compose.runtime.Immutable + +@Immutable +data class VoSpecialization( + val id: Int, + val title: String +) From 899e4e48b7bafbc91666ea18120443ab1db4dd99 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 10 Jan 2026 15:16:36 +0300 Subject: [PATCH 053/126] =?UTF-8?q?ANDR-5:=20VoSpecialization=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B8=20Loaded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt deleted file mode 100644 index 70627fa1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation - -import androidx.compose.runtime.Immutable - -@Immutable -data class VoSpecialization( - val id: Int, - val title: String -) From b2b790a3da40c850d3617cc3a0200ba16b295bf3 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:02 +0300 Subject: [PATCH 054/126] =?UTF-8?q?ANDR-5:=20Command'=D1=8B=20=D0=B8=20Res?= =?UTF-8?q?ult'=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20intents.=20=D0=A1?= =?UTF-8?q?=D0=B0=D0=BC=D0=B0=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ---------- .../presentation/intent/CreateQuizEvent.kt | 16 ---------------- .../presentation/intent/CreateQuizResult.kt | 10 ---------- 3 files changed, 36 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt deleted file mode 100644 index 6093773c..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizCommand { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizCommand - - data object NavigateBack : CreateQuizCommand -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt deleted file mode 100644 index 57e2cde0..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizEvent { - data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent - - data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnStartInterviewQuizClick( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizEvent - - data object OnBackClick : CreateQuizEvent -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt deleted file mode 100644 index 33e18a3b..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizResult { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizResult - - data object NavigateBack : CreateQuizResult -} \ No newline at end of file From bda2c30960b6597e2e5211a2f93da56ba472ea47 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:20 +0300 Subject: [PATCH 055/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20=D1=8E=D0=B7=D0=BA=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt new file mode 100644 index 00000000..7c2b7f8e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +interface GetSpecializationListUseCase { + + suspend operator fun invoke(): DomainSpecializationListResponse +} \ No newline at end of file From a2f427e7648e56689f2218f3085ea272874739c9 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:28:04 +0300 Subject: [PATCH 056/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt deleted file mode 100644 index 7c2b7f8e..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -interface GetSpecializationListUseCase { - - suspend operator fun invoke(): DomainSpecializationListResponse -} \ No newline at end of file From f16904f57bb4b3b1c6e5cc46e153a576ecb92a99 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 23 Jan 2026 15:20:54 +0300 Subject: [PATCH 057/126] =?UTF-8?q?ANDR-55=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizState.kt | 25 +++ .../interviewQuiz/ui/InterviewQuizScreen.kt | 189 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt new file mode 100644 index 00000000..83314eb8 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -0,0 +1,25 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +import androidx.compose.runtime.Immutable + +sealed interface InterviewQuizState { + + /** Изначальное состояние */ + data object Loading : InterviewQuizState + + @Immutable + data class Loaded( + val questions: List, + val questionsCount: Int + ) : InterviewQuizState{ + + @Immutable + data class VoQuestion( + val id: Long, + val title: String, + val shortAnswer: String + ) + } + + data class Error(val throwable: Throwable) : InterviewQuizState +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt new file mode 100644 index 00000000..6c040dfd --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -0,0 +1,189 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.Theme +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.interview_trainer.impl.R + +@Composable +private fun ScreenUI( + headerText: TextOrResource +) { + + val currentQuestion = 10 + val questionsCount = 45 + + Scaffold( + containerColor = Theme.colors.black10, + topBar = { + TopAppBarWithBottomBorder( + title = headerText, + onBackClick = { TODO("onBackClick don't implemented") } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues).padding(16.dp) + ) { + QuizProgress( + currentQuestion, + questionsCount + ) + + Spacer(Modifier.height(28.dp)) + + QuizCard() + } + + } +} + +@Composable +private fun QuizProgress( + current: Int, + total: Int, + modifier: Modifier = Modifier +) { + + val progress = (current.toFloat() / total).coerceIn(0f, 1f) + + Card( + modifier = modifier.fillMaxWidth().height(80.dp), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Column(modifier = Modifier.padding(16.dp)) { + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(RoundedCornerShape(100)), + color = Theme.colors.purple800, + trackColor = Theme.colors.purple300, + gapSize = (-8).dp, + strokeCap = StrokeCap.Round, + drawStopIndicator = {} + ) + + Spacer(Modifier.height(12.dp)) + + Text( + text = TextOrResource.Text("$current из $total").text, + color = Theme.colors.black500, + modifier = Modifier.align(Alignment.End) + ) + } + } + +} + +@Composable +private fun QuizCard( + modifier: Modifier = Modifier +) { + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Theme.colors.white900 + ), + shape = RoundedCornerShape(16), + elevation = CardDefaults.cardElevation(8.dp) + ) { + + Row { + NavQuizButton( + text = TextOrResource.Text("Назад"), + onClick = { TODO() }, + leadingIcon = null + ) + Spacer(Modifier.weight(1f)) + NavQuizButton( + text = TextOrResource.Text("Далее"), + onClick = { TODO() }, + leadingIcon = null + ) + } + } +} + +@Composable +private fun NavQuizButton( + text: TextOrResource, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding +) { + + val context = LocalContext.current + + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + contentPadding = contentPadding + ) { + if (leadingIcon != null) { + //Icon + } + Text( + text = when (text) { + is TextOrResource.Resource -> text.getString(context) + is TextOrResource.Text -> text.text + }, + color = Theme.colors.purple800 + ) + if (trailingIcon != null) { + // Icon + + } + } +} + +@StaticPreview +@Composable +fun InterviewQuizScreen() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + +@Preview(showBackground = true) +@Composable +fun DynamicPreviewUI() { + ScreenUI( + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) +} + From bb86320914691c6d5181186f5c635e07a0d74df1 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Sat, 24 Jan 2026 19:32:46 +0300 Subject: [PATCH 058/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable-nodpi/arrow_left_alt.xml | 10 ++++++++++ .../src/main/res/drawable-nodpi/arrow_right_alt.xml | 10 ++++++++++ .../res/drawable-nodpi/favorite_outlined_icon.xml | 10 ++++++++++ .../main/res/drawable-nodpi/thumbs_down_icon.xml | 9 +++++++++ .../src/main/res/drawable-nodpi/thumbs_up_icon.xml | 9 +++++++++ .../impl/src/main/res/drawable/ellipse_icon.xml | 9 +++++++++ .../src/main/res/drawable/favorite_filled_icon.xml | 13 +++++++++++++ 7 files changed, 70 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml new file mode 100644 index 00000000..cbc3319d --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml new file mode 100644 index 00000000..8fe08e66 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml new file mode 100644 index 00000000..7d6e597c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml new file mode 100644 index 00000000..dc4be875 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml new file mode 100644 index 00000000..0b07152f --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml new file mode 100644 index 00000000..c0481dc1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml new file mode 100644 index 00000000..7cba0263 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml @@ -0,0 +1,13 @@ + + + + From c1e303ae16b3684d94a8ab05efbe240718a28e8e Mon Sep 17 00:00:00 2001 From: Deyryl Date: Sat, 24 Jan 2026 19:34:11 +0300 Subject: [PATCH 059/126] =?UTF-8?q?ANDR-55:=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0,=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B2=20QuizState=20enum=20class,=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizState.kt | 7 +- .../interviewQuiz/ui/InterviewQuizScreen.kt | 436 +++++++++++++++--- .../ui/InterviewQuizScreenLoading.kt | 2 + 3 files changed, 378 insertions(+), 67 deletions(-) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt index 83314eb8..15690ef0 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -10,9 +10,14 @@ sealed interface InterviewQuizState { @Immutable data class Loaded( val questions: List, - val questionsCount: Int + val questionsCount: Int, + val currentQuestion: Int, + val isAnswerVisible: Boolean, + val answers: Map ) : InterviewQuizState{ + enum class QuizAnswer { KNOWN, UNKNOWN, NOTHING } + @Immutable data class VoQuestion( val id: Long, diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 6c040dfd..0f8e0309 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -1,16 +1,27 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.ui +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues 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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -20,24 +31,35 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import ru.yeahub.core_ui.component.ErrorScreen +import ru.yeahub.core_ui.component.SecondaryButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder +import ru.yeahub.core_ui.component.YeahubButtonDefaults import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState + +private val FIGMA_MEDIUM_PADDING = 16.dp +private val FIGMA_LOW_PADDING = 8.dp +private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp + +private val FIGMA_CARD_ELEVATION = 4.dp +private val FIGMA_RADIUS = 12.dp @Composable private fun ScreenUI( - headerText: TextOrResource + headerText: TextOrResource, + state: InterviewQuizState ) { - val currentQuestion = 10 - val questionsCount = 45 - Scaffold( containerColor = Theme.colors.black10, topBar = { @@ -47,19 +69,77 @@ private fun ScreenUI( ) } ) { paddingValues -> - Column( - modifier = Modifier.padding(paddingValues).padding(16.dp) - ) { - QuizProgress( - currentQuestion, - questionsCount - ) + Box(Modifier.padding(paddingValues)) { + when (state) { + is InterviewQuizState.Loaded -> { + val currentQuestion = state.questions[state.currentQuestion] + val currentQuestionNumber = state.questions.indexOf(currentQuestion) + 1 - Spacer(Modifier.height(28.dp)) - - QuizCard() + BaseQuizScreen( + currentQuestionNumber = currentQuestionNumber, + questionsCount = state.questions.count(), + questionText = currentQuestion.title, + shortAnswer = currentQuestion.shortAnswer, + isAnswerVisible = state.isAnswerVisible, + isBackClickable = false, + onBackClick = {}, + isNextClickable = false, + onNextClick = {}, + isFavorite = false, + onFavoriteClick = {} + ) + } + is InterviewQuizState.Error -> { + ErrorScreen( + error = state.throwable.localizedMessage, + errorText = TextOrResource.Resource(R.string.error_screen_text), + titleText = TextOrResource.Resource(R.string.title_error_screen_text), + backText = TextOrResource.Resource(R.string.back_error_screen_text), + unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), + onBack = { TODO() } + ) + } + InterviewQuizState.Loading -> {} + } } + } +} + +@Composable +private fun BaseQuizScreen( + currentQuestionNumber: Int, + questionsCount: Int, + questionText: String, + shortAnswer: String, + isAnswerVisible: Boolean, + isBackClickable: Boolean, + onBackClick: () -> Unit, + isNextClickable: Boolean, + onNextClick: () -> Unit, + isFavorite: Boolean, + onFavoriteClick: () -> Unit +) { + Column( + modifier = Modifier.padding( + start = FIGMA_MEDIUM_PADDING, + end = FIGMA_MEDIUM_PADDING, + top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING + ) + ) { + QuizProgress(currentQuestionNumber, questionsCount) + Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) + QuestionCard( + questionText = questionText, + shortAnswer = shortAnswer, + isAnswerVisible = isAnswerVisible, + isBackClickable = isBackClickable, + onBackClick = onBackClick, + isNextClickable = isNextClickable, + onNextClick = onNextClick, + isFavorite = isFavorite, + onFavoriteClick = onFavoriteClick + ) } } @@ -70,85 +150,197 @@ private fun QuizProgress( modifier: Modifier = Modifier ) { - val progress = (current.toFloat() / total).coerceIn(0f, 1f) - - Card( - modifier = modifier.fillMaxWidth().height(80.dp), - colors = CardDefaults.cardColors( - containerColor = Theme.colors.white900 - ), - shape = RoundedCornerShape(16), - elevation = CardDefaults.cardElevation(8.dp) - ) { + val progress = (current.toFloat() / total) - Column(modifier = Modifier.padding(16.dp)) { + DefaultCard(modifier) { + Column(modifier = Modifier.padding(FIGMA_MEDIUM_PADDING)) { LinearProgressIndicator( progress = { progress }, modifier = Modifier .fillMaxWidth() - .height(10.dp) - .clip(RoundedCornerShape(100)), - color = Theme.colors.purple800, + .height(8.dp) + .clip(RoundedCornerShape(24)), + color = Theme.colors.purple700, trackColor = Theme.colors.purple300, gapSize = (-8).dp, strokeCap = StrokeCap.Round, drawStopIndicator = {} ) - - Spacer(Modifier.height(12.dp)) - + Spacer(Modifier.height(8.dp)) Text( text = TextOrResource.Text("$current из $total").text, color = Theme.colors.black500, + style = Theme.typography.body2Accent, modifier = Modifier.align(Alignment.End) ) } } - } @Composable -private fun QuizCard( - modifier: Modifier = Modifier +private fun QuestionCard( + questionText: String, + shortAnswer: String, + isAnswerVisible: Boolean, + isBackClickable: Boolean, + onBackClick: () -> Unit, + isNextClickable: Boolean, + onNextClick: () -> Unit, + isFavorite: Boolean, + onFavoriteClick: () -> Unit ) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Theme.colors.white900 - ), - shape = RoundedCornerShape(16), - elevation = CardDefaults.cardElevation(8.dp) - ) { + val favoriteIcon: Painter = + painterResource( + if (isFavorite) R.drawable.favorite_filled_icon + else R.drawable.favorite_outlined_icon + ) - Row { - NavQuizButton( - text = TextOrResource.Text("Назад"), - onClick = { TODO() }, - leadingIcon = null + DefaultCard { + Column(Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING)) { + Row(Modifier.fillMaxWidth()) { + NavigationButton( + text = TextOrResource.Text("Назад"), + enabled = isBackClickable, + onClick = onBackClick, + leadingIcon = painterResource(R.drawable.arrow_left_alt) + ) + Spacer(Modifier.weight(1f)) + NavigationButton( + text = TextOrResource.Text("Далее"), + enabled = isNextClickable, + onClick = onNextClick, + trailingIcon = painterResource(R.drawable.arrow_right_alt) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = FIGMA_MEDIUM_PADDING), + verticalAlignment = Alignment.Top + + ) { + Icon( + painter = painterResource(R.drawable.ellipse_icon), + contentDescription = null, + modifier = Modifier.padding(top = 4.dp), + tint = Theme.colors.purple800 + ) + Text( + text = questionText, + modifier = Modifier + .padding(start = FIGMA_LOW_PADDING, end = 12.dp) + .weight(1f), + style = Theme.typography.body3Strong + ) + FilledIconButton( + onClick = onFavoriteClick, + modifier = Modifier.size(48.dp), + shape = RoundedCornerShape(FIGMA_RADIUS), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = Theme.colors.black10 + ) + ) { + Icon( + painter = favoriteIcon, + contentDescription = null, + modifier = Modifier.padding(12.dp), + tint = if (isFavorite) { + Theme.colors.red700 + } else { + Theme.colors.black600 + } + ) + } + } + Text( + text = if (isAnswerVisible) { + "Свернуть ответ" + } else { + "Посмотреть ответ" + }, + modifier = Modifier + .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp, bottom = 12.dp) + .clickable(onClick = { TODO() } ), + style = Theme.typography.body2, + color = Theme.colors.purple700 ) - Spacer(Modifier.weight(1f)) - NavQuizButton( - text = TextOrResource.Text("Далее"), - onClick = { TODO() }, - leadingIcon = null + if (isAnswerVisible) { + Text( + text = shortAnswer, + style = Theme.typography.body3 + ) + } + Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) + + Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) { + QuizAnswerButton( + painter = painterResource(R.drawable.thumbs_down_icon), + text = TextOrResource.Text("Не знаю"), + onClick = { TODO() }, + isSelected = false + ) + Spacer(Modifier.weight(1f)) + QuizAnswerButton( + painter = painterResource(R.drawable.thumbs_up_icon), + text = TextOrResource.Text("Знаю"), + onClick = { TODO() }, + isSelected = false + ) + } + HorizontalDivider( + modifier = Modifier.padding(bottom = FIGMA_MEDIUM_PADDING), + color = Theme.colors.black100 ) + SecondaryButton( + onClick = {}, + modifier = Modifier.width(170.dp).height(48.dp).align(Alignment.End), + colors = YeahubButtonDefaults.secondaryButtonColors( + containerColor = Theme.colors.red100, + contentColor = Theme.colors.red700 + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Завершить", + style = Theme.typography.body3Strong + ) + } + } } } } @Composable -private fun NavQuizButton( +private fun DefaultCard( + modifier: Modifier = Modifier, + content: @Composable (ColumnScope.() -> Unit) +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = Theme.colors.white900), + shape = RoundedCornerShape(FIGMA_RADIUS), + elevation = CardDefaults.cardElevation(FIGMA_CARD_ELEVATION), + content = content + ) +} + +@Composable +private fun NavigationButton( text: TextOrResource, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - leadingIcon: ImageVector? = null, - trailingIcon: ImageVector? = null, - contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding + leadingIcon: Painter? = null, + trailingIcon: Painter? = null, + contentPadding: PaddingValues = PaddingValues() ) { val context = LocalContext.current + val color = if (enabled) Theme.colors.purple700 else Theme.colors.purple300 TextButton( onClick = onClick, @@ -157,33 +349,145 @@ private fun NavQuizButton( contentPadding = contentPadding ) { if (leadingIcon != null) { - //Icon + Icon( + painter = leadingIcon, + contentDescription = null, + tint = color + ) + Spacer(Modifier.width(FIGMA_LOW_PADDING)) } Text( text = when (text) { is TextOrResource.Resource -> text.getString(context) is TextOrResource.Text -> text.text }, - color = Theme.colors.purple800 + color = color, + style = Theme.typography.body3Strong ) if (trailingIcon != null) { - // Icon - + Spacer(Modifier.width(FIGMA_LOW_PADDING)) + Icon( + painter = trailingIcon, + contentDescription = null, + tint = color + ) } } } +@Composable +private fun NavQuizIcon(painter: Painter) { + +} + +@Composable +private fun QuizAnswerButton( + painter: Painter, + text: TextOrResource, + onClick: () -> Unit, + isSelected: Boolean = false +) { + + val context = LocalContext.current + + val contentColor = if (isSelected) { + Theme.colors.purple700 + } else { + Theme.colors.black700 + } + + Button( + onClick = onClick, + modifier = Modifier.width(120.dp).height(48.dp), + shape = RoundedCornerShape(FIGMA_RADIUS), + colors = ButtonDefaults.buttonColors( + containerColor = Theme.colors.black10, + contentColor = contentColor + ), + contentPadding = PaddingValues( + horizontal = 12.dp, + vertical = FIGMA_LOW_PADDING + ) + ) { + Icon( + painter = painter, + contentDescription = null, + modifier = Modifier.padding(end = FIGMA_LOW_PADDING) + ) + Text( + text = when (text) { + is TextOrResource.Text -> text.text + is TextOrResource.Resource -> text.getString(context) + }, + style = Theme.typography.body2 + ) + } +} + +private val questions = listOf( + InterviewQuizState.Loaded.VoQuestion( + id = 12, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 14, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 9, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ), + InterviewQuizState.Loaded.VoQuestion( + id = 5, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." + ) +) + +private val answers = mapOf( + 12.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 14.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 9.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, + 5.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, +) + +class QuizScreenStateParamProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + InterviewQuizState.Loaded( + questions = questions, + questionsCount = questions.count(), + currentQuestion = 2, + isAnswerVisible = true, + answers = answers + ), + InterviewQuizState.Loading, + InterviewQuizState.Error( + Throwable("Не удалось загрузить данные") + ) + ) +} + @StaticPreview @Composable -fun InterviewQuizScreen() { +fun InterviewQuizScreen( + @PreviewParameter(QuizScreenStateParamProvider::class) + state: InterviewQuizState +) { ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), + state + ) } +/* @Preview(showBackground = true) @Composable fun DynamicPreviewUI() { ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)) + TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) + ) } - +*/ diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt new file mode 100644 index 00000000..3c9e0d23 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -0,0 +1,2 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.ui + From 32c8def7019a9ed392aa02079dbb4c0bd38c1f7b Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:05 +0300 Subject: [PATCH 060/126] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D1=8C=D1=8E=20=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0=20-=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ++++++++++ .../presentation/intent/CreateQuizEvent.kt | 16 ++++++++++++++++ .../presentation/intent/CreateQuizResult.kt | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt new file mode 100644 index 00000000..6093773c --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizCommand { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizCommand + + data object NavigateBack : CreateQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt new file mode 100644 index 00000000..57e2cde0 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt @@ -0,0 +1,16 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizEvent { + data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent + + data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent + + data class OnStartInterviewQuizClick( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizEvent + + data object OnBackClick : CreateQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt new file mode 100644 index 00000000..33e18a3b --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt @@ -0,0 +1,10 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent + +sealed interface CreateQuizResult { + data class NavigateToInterviewQuizScreen( + val specializationId: Long, + val questionCount: Int + ) : CreateQuizResult + + data object NavigateBack : CreateQuizResult +} \ No newline at end of file From bb9cd66a0cae9421e56679e43327fd1d4af0b58d Mon Sep 17 00:00:00 2001 From: PanMobile Date: Thu, 8 Jan 2026 21:02:48 +0300 Subject: [PATCH 061/126] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D0=BC=D1=8B=D1=85=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B0=20CreateQuiz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt new file mode 100644 index 00000000..70627fa1 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt @@ -0,0 +1,9 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.presentation + +import androidx.compose.runtime.Immutable + +@Immutable +data class VoSpecialization( + val id: Int, + val title: String +) From 4e9b78b3fb53ec34868cb9249348e26bd0e3d470 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 10 Jan 2026 15:16:36 +0300 Subject: [PATCH 062/126] =?UTF-8?q?ANDR-5:=20VoSpecialization=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B8=20Loaded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/presentation/VoSpecialization.kt | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt deleted file mode 100644 index 70627fa1..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/VoSpecialization.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation - -import androidx.compose.runtime.Immutable - -@Immutable -data class VoSpecialization( - val id: Int, - val title: String -) From 7d9ad90bbbde667a98f6b6b2425796b5c039d88e Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:02 +0300 Subject: [PATCH 063/126] =?UTF-8?q?ANDR-5:=20Command'=D1=8B=20=D0=B8=20Res?= =?UTF-8?q?ult'=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20intents.=20=D0=A1?= =?UTF-8?q?=D0=B0=D0=BC=D0=B0=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/intent/CreateQuizCommand.kt | 10 ---------- .../presentation/intent/CreateQuizEvent.kt | 16 ---------------- .../presentation/intent/CreateQuizResult.kt | 10 ---------- 3 files changed, 36 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt deleted file mode 100644 index 6093773c..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizCommand.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizCommand { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizCommand - - data object NavigateBack : CreateQuizCommand -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt deleted file mode 100644 index 57e2cde0..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizEvent { - data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent - - data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent - - data class OnStartInterviewQuizClick( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizEvent - - data object OnBackClick : CreateQuizEvent -} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt deleted file mode 100644 index 33e18a3b..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/intent/CreateQuizResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.presentation.intent - -sealed interface CreateQuizResult { - data class NavigateToInterviewQuizScreen( - val specializationId: Long, - val questionCount: Int - ) : CreateQuizResult - - data object NavigateBack : CreateQuizResult -} \ No newline at end of file From 016a98a6f21c557126193d4219383abd6217c695 Mon Sep 17 00:00:00 2001 From: PanMobile Date: Sat, 17 Jan 2026 20:55:20 +0300 Subject: [PATCH 064/126] =?UTF-8?q?ANDR-5:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20=D1=8E=D0=B7=D0=BA=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt new file mode 100644 index 00000000..7c2b7f8e --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt @@ -0,0 +1,6 @@ +package ru.yeahub.interview_trainer.impl.createQuiz.domain + +interface GetSpecializationListUseCase { + + suspend operator fun invoke(): DomainSpecializationListResponse +} \ No newline at end of file From 9be8a3a641795b0554f5a24aed55da64888cf97e Mon Sep 17 00:00:00 2001 From: PanMobile Date: Mon, 19 Jan 2026 18:28:04 +0300 Subject: [PATCH 065/126] =?UTF-8?q?ANDR-5:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/createQuiz/domain/GetSpecializationListUseCase.kt | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt deleted file mode 100644 index 7c2b7f8e..00000000 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationListUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.interview_trainer.impl.createQuiz.domain - -interface GetSpecializationListUseCase { - - suspend operator fun invoke(): DomainSpecializationListResponse -} \ No newline at end of file From 1c37828076bf39c5ec42d472a0a326d6c8273918 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Wed, 28 Jan 2026 16:28:09 +0300 Subject: [PATCH 066/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/src/main/res/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index c72f7ccf..d2385b5f 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -10,4 +10,12 @@ Что‑то пошло не так Назад Не удалось загрузить данные + Проверить результаты + Свернуть ответ + Показать ответ + Не знаю + Знаю + Завершить + Назад + Далее \ No newline at end of file From 1729b9ebfb03a073a27deaa46ae8355f6f49f4b7 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Thu, 29 Jan 2026 12:04:05 +0300 Subject: [PATCH 067/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20LoadingScreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/InterviewQuizScreenLoading.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt index 3c9e0d23..0f086f3b 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -1,2 +1,53 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.valentinilk.shimmer.shimmer +import ru.yeahub.core_ui.example.staticPreview.StaticPreview +import ru.yeahub.core_ui.theme.Theme + +@Composable +fun InterviewQuizLoading() { + + Column(Modifier.padding(horizontal = 16.dp).fillMaxSize()) { + PlaceHolderBlock( + Modifier.padding(vertical = 24.dp).fillMaxWidth().height(65.dp) + ) + + PlaceHolderBlock(Modifier.fillMaxWidth().height(320.dp)) + + + } +} + +@Composable +private fun PlaceHolderBlock(modifier: Modifier = Modifier) { + + Card( + modifier = modifier.shimmer(), + colors = CardDefaults.cardColors(containerColor = Theme.colors.white900), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Box(Modifier.fillMaxSize().background(Color.LightGray)) + } +} + +@StaticPreview +@Composable +private fun QuizLoadingStaticPreview() { + + InterviewQuizLoading() +} \ No newline at end of file From fe13beefee3ad5435fe9aff7957cda3c0dfe4a76 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:27:44 +0300 Subject: [PATCH 068/126] =?UTF-8?q?ANDR-55:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Command,=20Event,=20Scre?= =?UTF-8?q?enMapper,=20State?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizCommand.kt | 8 ++++++ .../presentation/InterviewQuizEvent.kt | 22 ++++++++++++++++ .../presentation/InterviewQuizScreenMapper.kt | 18 +++++++++++++ .../presentation/InterviewQuizState.kt | 25 ++++++++++++++++--- 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt new file mode 100644 index 00000000..fcde5b25 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt @@ -0,0 +1,8 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +sealed interface InterviewQuizCommand { + + data object NavigateToInterviewQuizResultScreen : InterviewQuizCommand + + data object NavigateBack : InterviewQuizCommand +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt new file mode 100644 index 00000000..471d5934 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt @@ -0,0 +1,22 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + + + +sealed interface InterviewQuizEvent { + + data object OnShowResultClick : InterviewQuizEvent + + data object OnKnownAnswerClick : InterviewQuizEvent + + data object OnUnknownAnswerClick : InterviewQuizEvent + + data object OnNextQuestionClick : InterviewQuizEvent + + data object OnPreviousQuestionClick : InterviewQuizEvent + + data object OnFavoriteQuestionClick : InterviewQuizEvent + + data object OnShowHideAnswerClick : InterviewQuizEvent + + data object OnBackClick : InterviewQuizEvent +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt new file mode 100644 index 00000000..3073ccc8 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt @@ -0,0 +1,18 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +object InterviewQuizScreenMapper { + + fun getScreenState( + questions: List, + questionsCount: Int, + currentQuestion: Int, + isAnswerVisible: Boolean, + answers: Map + ): InterviewQuizState = InterviewQuizState.Loaded( + questions = questions, + questionsCount = questionsCount, + currentQuestionIndex = currentQuestion, + isAnswerVisible = isAnswerVisible, + answers = answers + ) +} \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt index 15690ef0..e001497b 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -11,12 +11,29 @@ sealed interface InterviewQuizState { data class Loaded( val questions: List, val questionsCount: Int, - val currentQuestion: Int, - val isAnswerVisible: Boolean, - val answers: Map + val currentQuestionIndex: Int, + val isAnswerVisible: Boolean = false, + val answers: Map = emptyMap(), ) : InterviewQuizState{ - enum class QuizAnswer { KNOWN, UNKNOWN, NOTHING } + val canGoNext: Boolean + get() { + return if (answers.containsKey(questions[currentQuestionIndex].id) + && currentQuestionIndex != questions.lastIndex) { + true + } else { + false + } + } + + val canGoPrevious: Boolean + get() = currentQuestionIndex > 0 + + val currentAnswer: QuizAnswer + get() = answers[questions[currentQuestionIndex].id] + ?: QuizAnswer.NONE + + enum class QuizAnswer { KNOWN, UNKNOWN, NONE } @Immutable data class VoQuestion( From bb326f07b3bd67dd29269f62921870fc7ae92fdb Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:27:56 +0300 Subject: [PATCH 069/126] =?UTF-8?q?ANDR-55:=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20string=20=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=83=D1=80=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/interview-trainer/impl/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml index d2385b5f..26739f27 100644 --- a/feature/interview-trainer/impl/src/main/res/values/strings.xml +++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml @@ -16,6 +16,6 @@ Не знаю Знаю Завершить - Назад + Назад Далее \ No newline at end of file From f2feafadfb5ba6b4b3a55abc6c1795b7d053c23e Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:28:42 +0300 Subject: [PATCH 070/126] =?UTF-8?q?ANDR-55:=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=BD=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20ViewModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizViewModel.kt | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt new file mode 100644 index 00000000..be0244b7 --- /dev/null +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt @@ -0,0 +1,170 @@ +package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.yeahub.core_utils.BaseViewModel + +open class InterviewQuizViewModel( + private val screenMapper: InterviewQuizScreenMapper +) : BaseViewModel() { + + private val _screenState = MutableStateFlow( + InterviewQuizState.Loading + ) + + val screenState = _screenState.stateIn( + scope = viewModelScopeSafe, + started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES), + initialValue = InterviewQuizState.Loading + ) + + private val _commands = MutableSharedFlow() + val commands = _commands.asSharedFlow() + + init { + viewModelScopeSafe.launch { + delay(RESPONSE_DELAY) + initialLoad() + } + } + + fun onEvent(event: InterviewQuizEvent) { + when (event) { + InterviewQuizEvent.OnBackClick -> onBackClick() + InterviewQuizEvent.OnKnownAnswerClick -> onKnownAnswerClick() + InterviewQuizEvent.OnUnknownAnswerClick -> onUnknownAnswerClick() + InterviewQuizEvent.OnShowResultClick -> onShowResultClick() + InterviewQuizEvent.OnFavoriteQuestionClick -> { /* TODO: нет профиля */ } + InterviewQuizEvent.OnNextQuestionClick -> onNextQuestionClick() + InterviewQuizEvent.OnPreviousQuestionClick -> onPreviousQuestionClick() + InterviewQuizEvent.OnShowHideAnswerClick -> onShowHideAnswerClick() + } + } + + private fun onShowHideAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + loaded.copy(isAnswerVisible = !loaded.isAnswerVisible) + } + } + } + + private fun onPreviousQuestionClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + if (loaded.canGoPrevious) { + loaded.copy( + currentQuestionIndex = loaded.currentQuestionIndex - 1, + isAnswerVisible = false + ) + } else { + loaded + } + } + } + } + + private fun onNextQuestionClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + if (loaded.canGoNext) { + loaded.copy( + currentQuestionIndex = loaded.currentQuestionIndex + 1, + isAnswerVisible = false + ) + } else { + loaded + } + } + } + } + + /** Пока нет domain/data - загрузка Мок данных */ + protected open suspend fun initialLoad() { + val questions = previewQuestions() + _screenState.value = InterviewQuizState.Loaded( + questions = questions, + questionsCount = questions.count(), + currentQuestionIndex = 0, + isAnswerVisible = false, + ) + } + + private fun onBackClick() { + viewModelScopeSafe.launch { + _commands.emit(InterviewQuizCommand.NavigateBack) + } + } + + private fun onKnownAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) + ?: return@updateLoaded loaded + loaded.copy( + answers = loaded.answers + + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.KNOWN) + ) + } + } + } + + private fun onUnknownAnswerClick() { + viewModelScopeSafe.launch { + _screenState.updateLoaded { loaded -> + val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) + ?: return@updateLoaded loaded + loaded.copy( + answers = loaded.answers + + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN) + ) + } + } + } + + private fun onShowResultClick() { + viewModelScopeSafe.launch(Dispatchers.IO) { + _commands.emit( + InterviewQuizCommand.NavigateToInterviewQuizResultScreen + ) + } + } + + /** Метод-хелпер для обновления состояния Loaded */ + private inline fun MutableStateFlow.updateLoaded( + transform: (InterviewQuizState.Loaded) -> InterviewQuizState.Loaded + ) { + update { state -> + val loaded = state as? InterviewQuizState.Loaded ?: return@update state + transform(loaded) + } + } + + /** Создание списка вопросов для тестирования превью */ + private fun previewQuestions(): List { + val base1 = InterviewQuizState.Loaded.VoQuestion(0, "Что такое Virtual DOM, и как он работает?", "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений.") + val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") + val questions = mutableListOf() + repeat(10) { index -> + if (index % 2 == 0) { + questions.add(base1.copy(id = index.toLong())) + } else { + questions.add(base2.copy(id = index.toLong())) + } + } + return questions + } + + companion object { + + private const val RESPONSE_DELAY = 2500L + private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L + } +} From 00839953e2b3aa6bb5d98357424e2bd184f52f72 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 14:29:57 +0300 Subject: [PATCH 071/126] =?UTF-8?q?ANDR-55:=20=D0=BE=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0.=20=D0=A1=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interviewQuiz/ui/InterviewQuizScreen.kt | 326 ++++++++++++------ 1 file changed, 213 insertions(+), 113 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 0f8e0309..9b3e909a 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -27,6 +27,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -34,10 +40,16 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel import ru.yeahub.core_ui.component.ErrorScreen +import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.SecondaryButton import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder import ru.yeahub.core_ui.component.YeahubButtonDefaults @@ -45,7 +57,10 @@ import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.interview_trainer.impl.R +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizEvent +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizScreenMapper import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizViewModel private val FIGMA_MEDIUM_PADDING = 16.dp private val FIGMA_LOW_PADDING = 8.dp @@ -54,10 +69,35 @@ private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp private val FIGMA_CARD_ELEVATION = 4.dp private val FIGMA_RADIUS = 12.dp +private data class QuestionCardState( + val questionText: String, + val shortAnswer: String, + val currentAnswer: InterviewQuizState.Loaded.QuizAnswer, + val isAnswerVisible: Boolean, + val canGoPrevious: Boolean, + val canGoNext: Boolean, + val isLastQuestion: Boolean +) + +private fun InterviewQuizState.Loaded.toQuestionCardState(): QuestionCardState { + val question = questions[currentQuestionIndex] + + return QuestionCardState( + questionText = question.title, + shortAnswer = question.shortAnswer, + currentAnswer = currentAnswer, + isAnswerVisible = isAnswerVisible, + canGoPrevious = canGoPrevious, + canGoNext = canGoNext, + isLastQuestion = currentQuestionIndex == questions.lastIndex + ) +} + @Composable private fun ScreenUI( headerText: TextOrResource, - state: InterviewQuizState + state: InterviewQuizState, + onEvent: (InterviewQuizEvent) -> Unit ) { Scaffold( @@ -71,35 +111,38 @@ private fun ScreenUI( ) { paddingValues -> Box(Modifier.padding(paddingValues)) { when (state) { - is InterviewQuizState.Loaded -> { - val currentQuestion = state.questions[state.currentQuestion] - val currentQuestionNumber = state.questions.indexOf(currentQuestion) + 1 - - BaseQuizScreen( - currentQuestionNumber = currentQuestionNumber, - questionsCount = state.questions.count(), - questionText = currentQuestion.title, - shortAnswer = currentQuestion.shortAnswer, - isAnswerVisible = state.isAnswerVisible, - isBackClickable = false, - onBackClick = {}, - isNextClickable = false, - onNextClick = {}, - isFavorite = false, - onFavoriteClick = {} - ) - } - is InterviewQuizState.Error -> { - ErrorScreen( - error = state.throwable.localizedMessage, - errorText = TextOrResource.Resource(R.string.error_screen_text), - titleText = TextOrResource.Resource(R.string.title_error_screen_text), - backText = TextOrResource.Resource(R.string.back_error_screen_text), - unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), - onBack = { TODO() } - ) - } - InterviewQuizState.Loading -> {} + is InterviewQuizState.Loaded -> BaseQuizScreen( + state = state, + onPreviousClick = { + onEvent(InterviewQuizEvent.OnPreviousQuestionClick) + }, + onNextClick = { + onEvent(InterviewQuizEvent.OnNextQuestionClick) + }, + onUnknownClick = { + onEvent(InterviewQuizEvent.OnUnknownAnswerClick) + }, + onKnownClick = { + onEvent(InterviewQuizEvent.OnKnownAnswerClick) + }, + onShowHideAnswerClick = { + onEvent(InterviewQuizEvent.OnShowHideAnswerClick) + }, + onResultClick = { + onEvent(InterviewQuizEvent.OnShowResultClick) + }, + ) + + is InterviewQuizState.Error -> ErrorScreen( + error = state.throwable.localizedMessage, + errorText = TextOrResource.Resource(R.string.error_screen_text), + titleText = TextOrResource.Resource(R.string.title_error_screen_text), + backText = TextOrResource.Resource(R.string.back_error_screen_text), + unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text), + onBack = { TODO() } + ) + + InterviewQuizState.Loading -> InterviewQuizLoading() } } } @@ -107,19 +150,17 @@ private fun ScreenUI( @Composable private fun BaseQuizScreen( - currentQuestionNumber: Int, - questionsCount: Int, - questionText: String, - shortAnswer: String, - isAnswerVisible: Boolean, - isBackClickable: Boolean, - onBackClick: () -> Unit, - isNextClickable: Boolean, + state: InterviewQuizState.Loaded, + onPreviousClick: () -> Unit, onNextClick: () -> Unit, - isFavorite: Boolean, - onFavoriteClick: () -> Unit + onUnknownClick: () -> Unit, + onKnownClick: () -> Unit, + onShowHideAnswerClick: () -> Unit, + onResultClick: () -> Unit ) { + val cardState = state.toQuestionCardState() + Column( modifier = Modifier.padding( start = FIGMA_MEDIUM_PADDING, @@ -127,18 +168,19 @@ private fun BaseQuizScreen( top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING ) ) { - QuizProgress(currentQuestionNumber, questionsCount) + QuizProgress( + current = state.currentQuestionIndex + 1, + total = state.questionsCount, + ) Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) QuestionCard( - questionText = questionText, - shortAnswer = shortAnswer, - isAnswerVisible = isAnswerVisible, - isBackClickable = isBackClickable, - onBackClick = onBackClick, - isNextClickable = isNextClickable, + state = cardState, + onPreviousClick = onPreviousClick, onNextClick = onNextClick, - isFavorite = isFavorite, - onFavoriteClick = onFavoriteClick + onUnknownClick = onUnknownClick, + onKnownClick = onKnownClick, + onShowAnswerClick = onShowHideAnswerClick, + onResultClick = onResultClick, ) } } @@ -179,17 +221,18 @@ private fun QuizProgress( @Composable private fun QuestionCard( - questionText: String, - shortAnswer: String, - isAnswerVisible: Boolean, - isBackClickable: Boolean, - onBackClick: () -> Unit, - isNextClickable: Boolean, + state: QuestionCardState, + onPreviousClick: () -> Unit, onNextClick: () -> Unit, - isFavorite: Boolean, - onFavoriteClick: () -> Unit + onUnknownClick: () -> Unit, + onKnownClick: () -> Unit, + onShowAnswerClick: () -> Unit, + onResultClick: () -> Unit ) { + /* TODO: нет фичи профиля */ + var isFavorite by rememberSaveable { mutableStateOf(false) } + val favoriteIcon: Painter = painterResource( if (isFavorite) R.drawable.favorite_filled_icon @@ -197,18 +240,20 @@ private fun QuestionCard( ) DefaultCard { - Column(Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING)) { + Column(Modifier + .fillMaxWidth() + .padding(FIGMA_MEDIUM_PADDING)) { Row(Modifier.fillMaxWidth()) { NavigationButton( - text = TextOrResource.Text("Назад"), - enabled = isBackClickable, - onClick = onBackClick, + text = TextOrResource.Resource(R.string.quiz_btn_prev), + enabled = state.canGoPrevious, + onClick = onPreviousClick, leadingIcon = painterResource(R.drawable.arrow_left_alt) ) Spacer(Modifier.weight(1f)) NavigationButton( - text = TextOrResource.Text("Далее"), - enabled = isNextClickable, + text = TextOrResource.Resource(R.string.quiz_btn_next), + enabled = state.canGoNext, onClick = onNextClick, trailingIcon = painterResource(R.drawable.arrow_right_alt) ) @@ -227,14 +272,14 @@ private fun QuestionCard( tint = Theme.colors.purple800 ) Text( - text = questionText, + text = state.questionText, modifier = Modifier .padding(start = FIGMA_LOW_PADDING, end = 12.dp) .weight(1f), style = Theme.typography.body3Strong ) FilledIconButton( - onClick = onFavoriteClick, + onClick = { isFavorite = !isFavorite }, modifier = Modifier.size(48.dp), shape = RoundedCornerShape(FIGMA_RADIUS), colors = IconButtonDefaults.filledIconButtonColors( @@ -254,20 +299,21 @@ private fun QuestionCard( } } Text( - text = if (isAnswerVisible) { - "Свернуть ответ" + text = if (state.isAnswerVisible) { + stringResource(R.string.quiz_collapse_answer) } else { - "Посмотреть ответ" + stringResource(R.string.quiz_show_answer) }, modifier = Modifier - .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp, bottom = 12.dp) - .clickable(onClick = { TODO() } ), + .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp) + .clickable(onClick = onShowAnswerClick), style = Theme.typography.body2, color = Theme.colors.purple700 ) - if (isAnswerVisible) { + if (state.isAnswerVisible) { Text( - text = shortAnswer, + text = state.shortAnswer, + modifier = Modifier.padding(top = 12.dp), style = Theme.typography.body3 ) } @@ -276,38 +322,59 @@ private fun QuestionCard( Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) { QuizAnswerButton( painter = painterResource(R.drawable.thumbs_down_icon), - text = TextOrResource.Text("Не знаю"), - onClick = { TODO() }, - isSelected = false + text = TextOrResource.Resource(R.string.quiz_answer_unknown), + onClick = onUnknownClick, + isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN ) Spacer(Modifier.weight(1f)) QuizAnswerButton( painter = painterResource(R.drawable.thumbs_up_icon), - text = TextOrResource.Text("Знаю"), - onClick = { TODO() }, - isSelected = false + text = TextOrResource.Resource(R.string.quiz_answer_known), + onClick = onKnownClick, + isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN ) } HorizontalDivider( modifier = Modifier.padding(bottom = FIGMA_MEDIUM_PADDING), color = Theme.colors.black100 ) - SecondaryButton( - onClick = {}, - modifier = Modifier.width(170.dp).height(48.dp).align(Alignment.End), - colors = YeahubButtonDefaults.secondaryButtonColors( - containerColor = Theme.colors.red100, - contentColor = Theme.colors.red700 - ) - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + if (state.isLastQuestion) { + PrimaryButton( + onClick = onResultClick, + modifier = Modifier.height(48.dp), + ) { - Text( - text = "Завершить", - style = Theme.typography.body3Strong + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.quiz_check_result), + style = Theme.typography.body3Strong + ) + } + } + } else { + SecondaryButton( + onClick = {}, + modifier = Modifier + .width(170.dp) + .height(48.dp) + .align(Alignment.End), + colors = YeahubButtonDefaults.secondaryButtonColors( + containerColor = Theme.colors.red100, + contentColor = Theme.colors.red700 ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.quiz_btn_complete), + style = Theme.typography.body3Strong + ) + } } } } @@ -375,17 +442,12 @@ private fun NavigationButton( } } -@Composable -private fun NavQuizIcon(painter: Painter) { - -} - @Composable private fun QuizAnswerButton( painter: Painter, text: TextOrResource, onClick: () -> Unit, - isSelected: Boolean = false + isSelected: Boolean ) { val context = LocalContext.current @@ -398,7 +460,9 @@ private fun QuizAnswerButton( Button( onClick = onClick, - modifier = Modifier.width(120.dp).height(48.dp), + modifier = Modifier + .width(120.dp) + .height(48.dp), shape = RoundedCornerShape(FIGMA_RADIUS), colors = ButtonDefaults.buttonColors( containerColor = Theme.colors.black10, @@ -426,32 +490,32 @@ private fun QuizAnswerButton( private val questions = listOf( InterviewQuizState.Loaded.VoQuestion( - id = 12, + id = 0, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 14, + id = 1, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 9, + id = 2, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ), InterviewQuizState.Loaded.VoQuestion( - id = 5, + id = 3, title = "Что такое Virtual DOM, и как он работает?", shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." ) ) private val answers = mapOf( - 12.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, - 14.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, - 9.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, - 5.toLong() to InterviewQuizState.Loaded.QuizAnswer.NOTHING, + 0.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.NONE, + 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, ) class QuizScreenStateParamProvider : PreviewParameterProvider { @@ -459,7 +523,21 @@ class QuizScreenStateParamProvider : PreviewParameterProvider { + InterviewQuizViewModel(InterviewQuizScreenMapper) + } + + val state by mockViewModel.screenState.collectAsState() + ScreenUI( - TextOrResource.Resource(R.string.create_quiz_top_bar_header_text) + headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text), + state = state, + onEvent = mockViewModel::onEvent ) } -*/ + +typealias ViewModelCreator = () -> ViewModel? + +class ViewModelFactory( + private val viewModelCreator: ViewModelCreator = { null }, +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = viewModelCreator() as T +} + +@Composable +inline fun viewModelCreator(noinline creator: ViewModelCreator): VM = + viewModel(factory = remember { ViewModelFactory(creator) }) \ No newline at end of file From 1f2604c7ccddac8682816680125106c9559c4405 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 16:28:20 +0300 Subject: [PATCH 072/126] =?UTF-8?q?ANDR-55:=20=D1=83=D0=B2=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/interviewQuiz/presentation/InterviewQuizViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt index be0244b7..c3e37c59 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt @@ -150,7 +150,7 @@ open class InterviewQuizViewModel( /** Создание списка вопросов для тестирования превью */ private fun previewQuestions(): List { val base1 = InterviewQuizState.Loaded.VoQuestion(0, "Что такое Virtual DOM, и как он работает?", "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений.") - val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") + val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") val questions = mutableListOf() repeat(10) { index -> if (index % 2 == 0) { From e43851afd2ea6b57898b5f8daa0a12255fc1ded1 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Fri, 30 Jan 2026 16:29:20 +0300 Subject: [PATCH 073/126] =?UTF-8?q?ANDR-55:=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3.=20=D0=9A=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=B0=20=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B0=D1=82=20=D0=BC.=D0=B1.=20=D0=BD=D0=B5=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interviewQuiz/ui/InterviewQuizScreen.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index 9b3e909a..a932dbf7 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -13,7 +13,9 @@ 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.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -162,11 +164,13 @@ private fun BaseQuizScreen( val cardState = state.toQuestionCardState() Column( - modifier = Modifier.padding( - start = FIGMA_MEDIUM_PADDING, - end = FIGMA_MEDIUM_PADDING, - top = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING - ) + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding( + horizontal = FIGMA_MEDIUM_PADDING, + vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING + ) ) { QuizProgress( current = state.currentQuestionIndex + 1, @@ -342,7 +346,10 @@ private fun QuestionCard( PrimaryButton( onClick = onResultClick, modifier = Modifier.height(48.dp), - + enabled = state.currentAnswer != InterviewQuizState.Loaded.QuizAnswer.NONE, + colors = YeahubButtonDefaults.primaryButtonColors( + disabledContentColor = Theme.colors.black100 + ) ) { Box( modifier = Modifier.fillMaxSize(), @@ -350,7 +357,8 @@ private fun QuestionCard( ) { Text( text = stringResource(R.string.quiz_check_result), - style = Theme.typography.body3Strong + style = Theme.typography.body3Strong, + color = Theme.colors.white900 ) } } @@ -512,10 +520,9 @@ private val questions = listOf( ) private val answers = mapOf( - 0.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, - 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.NONE, - 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, + 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN ) class QuizScreenStateParamProvider : PreviewParameterProvider { @@ -538,7 +545,7 @@ class QuizScreenStateParamProvider : PreviewParameterProvider Date: Fri, 30 Jan 2026 17:07:42 +0300 Subject: [PATCH 074/126] =?UTF-8?q?ANDR-55:=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= =?UTF-8?q?=20=D0=B8=D0=B7=20LoadingScreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt index 0f086f3b..ab2ccbdb 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.valentinilk.shimmer.shimmer -import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme @Composable @@ -43,11 +42,4 @@ private fun PlaceHolderBlock(modifier: Modifier = Modifier) { ) { Box(Modifier.fillMaxSize().background(Color.LightGray)) } -} - -@StaticPreview -@Composable -private fun QuizLoadingStaticPreview() { - - InterviewQuizLoading() } \ No newline at end of file From 77c65680e50c2676adc622e7f8eb14b1cb6e3763 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Wed, 4 Feb 2026 16:49:51 +0300 Subject: [PATCH 075/126] =?UTF-8?q?ANDR-58:=20=D0=B8=D0=B7=20Command=20?= =?UTF-8?q?=D0=B8=20Event=20=D0=BD=D0=B0=D1=81=D0=BB=D0=B5=D0=B4=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20ToDo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizCommand.kt | 4 +--- .../presentation/InterviewQuizEvent.kt | 18 +----------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt index fcde5b25..2fca28c0 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt @@ -2,7 +2,5 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation sealed interface InterviewQuizCommand { - data object NavigateToInterviewQuizResultScreen : InterviewQuizCommand - - data object NavigateBack : InterviewQuizCommand + data object ToDo : InterviewQuizCommand } \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt index 471d5934..63fdf560 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt @@ -1,22 +1,6 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation - - sealed interface InterviewQuizEvent { - data object OnShowResultClick : InterviewQuizEvent - - data object OnKnownAnswerClick : InterviewQuizEvent - - data object OnUnknownAnswerClick : InterviewQuizEvent - - data object OnNextQuestionClick : InterviewQuizEvent - - data object OnPreviousQuestionClick : InterviewQuizEvent - - data object OnFavoriteQuestionClick : InterviewQuizEvent - - data object OnShowHideAnswerClick : InterviewQuizEvent - - data object OnBackClick : InterviewQuizEvent + data object ToDo : InterviewQuizEvent } \ No newline at end of file From 6d61ef15eb96ee08eff6e293a7cc1b5c2f255233 Mon Sep 17 00:00:00 2001 From: Deyryl Date: Wed, 4 Feb 2026 16:50:52 +0300 Subject: [PATCH 076/126] =?UTF-8?q?ANDR-58:=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20QuizScreen=20?= =?UTF-8?q?=D0=B8=20QuizScreenLoading=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20ViewModel.=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B8=D0=B7=20ScreenState=20?= =?UTF-8?q?=D0=B2=20ScreenMapper.=20=D0=98=D0=B7=20=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=20ScreenState=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/InterviewQuizScreenMapper.kt | 40 ++-- .../presentation/InterviewQuizState.kt | 30 +-- .../presentation/InterviewQuizViewModel.kt | 179 +++++------------ .../interviewQuiz/ui/InterviewQuizScreen.kt | 181 +++++++----------- .../ui/InterviewQuizScreenLoading.kt | 13 -- 5 files changed, 159 insertions(+), 284 deletions(-) diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt index 3073ccc8..23e19f83 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt @@ -1,18 +1,36 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation -object InterviewQuizScreenMapper { +class InterviewQuizScreenMapper { fun getScreenState( questions: List, - questionsCount: Int, - currentQuestion: Int, + questionIndex: Int, isAnswerVisible: Boolean, - answers: Map - ): InterviewQuizState = InterviewQuizState.Loaded( - questions = questions, - questionsCount = questionsCount, - currentQuestionIndex = currentQuestion, - isAnswerVisible = isAnswerVisible, - answers = answers - ) + answers: Map, + selectedAnswer: InterviewQuizState.Loaded.QuizAnswer + ): InterviewQuizState { + val canGoNext = answers.containsKey(questions[questionIndex].id) && + questionIndex != questions.lastIndex + + val canGoPrev = questionIndex > 0 + + val question = questions[questionIndex] + + val questionsCount = questions.size + + val isLastQuestion = questionIndex != questions.lastIndex + + return InterviewQuizState.Loaded( + questions = questions, + questionsCount = questionsCount, + questionIndex = questionIndex, + question = question, + isAnswerVisible = isAnswerVisible, + answers = answers, + canGoNext = canGoNext, + canGoPrev = canGoPrev, + selectedAnswer = selectedAnswer, + isLastQuestion = isLastQuestion + ) + } } \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt index e001497b..efbdc0cf 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt @@ -11,27 +11,15 @@ sealed interface InterviewQuizState { data class Loaded( val questions: List, val questionsCount: Int, - val currentQuestionIndex: Int, - val isAnswerVisible: Boolean = false, - val answers: Map = emptyMap(), - ) : InterviewQuizState{ - - val canGoNext: Boolean - get() { - return if (answers.containsKey(questions[currentQuestionIndex].id) - && currentQuestionIndex != questions.lastIndex) { - true - } else { - false - } - } - - val canGoPrevious: Boolean - get() = currentQuestionIndex > 0 - - val currentAnswer: QuizAnswer - get() = answers[questions[currentQuestionIndex].id] - ?: QuizAnswer.NONE + val questionIndex: Int, + val question: VoQuestion, + val isAnswerVisible: Boolean, + val answers: Map, + val canGoPrev: Boolean, + val canGoNext: Boolean, + val selectedAnswer: QuizAnswer, + val isLastQuestion: Boolean + ) : InterviewQuizState { enum class QuizAnswer { KNOWN, UNKNOWN, NONE } diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt index c3e37c59..f70551de 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt @@ -1,25 +1,42 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import ru.yeahub.core_utils.BaseViewModel +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.QuizAnswer +import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.VoQuestion open class InterviewQuizViewModel( private val screenMapper: InterviewQuizScreenMapper ) : BaseViewModel() { - private val _screenState = MutableStateFlow( - InterviewQuizState.Loading + // Вопросы для превью. Временно + private val previewQuestions by lazy { + previewQuestions() + } + + private val userInputState = MutableStateFlow( + UserInput( + isAnswerVisible = false, + answers = emptyMap(), + selectedAnswer = QuizAnswer.NONE + ) ) - val screenState = _screenState.stateIn( + val screenState = userInputState + .map { userInput -> + screenMapper.getScreenState( + questions = previewQuestions, + questionIndex = FIRST_QUESTION_INDEX, + isAnswerVisible = userInput.isAnswerVisible, + answers = userInput.answers, + selectedAnswer = userInput.selectedAnswer + ) + }.stateIn( scope = viewModelScopeSafe, started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES), initialValue = InterviewQuizState.Loading @@ -28,143 +45,43 @@ open class InterviewQuizViewModel( private val _commands = MutableSharedFlow() val commands = _commands.asSharedFlow() - init { - viewModelScopeSafe.launch { - delay(RESPONSE_DELAY) - initialLoad() - } - } - fun onEvent(event: InterviewQuizEvent) { when (event) { - InterviewQuizEvent.OnBackClick -> onBackClick() - InterviewQuizEvent.OnKnownAnswerClick -> onKnownAnswerClick() - InterviewQuizEvent.OnUnknownAnswerClick -> onUnknownAnswerClick() - InterviewQuizEvent.OnShowResultClick -> onShowResultClick() - InterviewQuizEvent.OnFavoriteQuestionClick -> { /* TODO: нет профиля */ } - InterviewQuizEvent.OnNextQuestionClick -> onNextQuestionClick() - InterviewQuizEvent.OnPreviousQuestionClick -> onPreviousQuestionClick() - InterviewQuizEvent.OnShowHideAnswerClick -> onShowHideAnswerClick() - } - } - - private fun onShowHideAnswerClick() { - viewModelScopeSafe.launch { - _screenState.updateLoaded { loaded -> - loaded.copy(isAnswerVisible = !loaded.isAnswerVisible) - } - } - } - - private fun onPreviousQuestionClick() { - viewModelScopeSafe.launch { - _screenState.updateLoaded { loaded -> - if (loaded.canGoPrevious) { - loaded.copy( - currentQuestionIndex = loaded.currentQuestionIndex - 1, - isAnswerVisible = false - ) - } else { - loaded - } - } - } - } - - private fun onNextQuestionClick() { - viewModelScopeSafe.launch { - _screenState.updateLoaded { loaded -> - if (loaded.canGoNext) { - loaded.copy( - currentQuestionIndex = loaded.currentQuestionIndex + 1, - isAnswerVisible = false - ) - } else { - loaded - } - } - } - } - - /** Пока нет domain/data - загрузка Мок данных */ - protected open suspend fun initialLoad() { - val questions = previewQuestions() - _screenState.value = InterviewQuizState.Loaded( - questions = questions, - questionsCount = questions.count(), - currentQuestionIndex = 0, - isAnswerVisible = false, - ) - } - - private fun onBackClick() { - viewModelScopeSafe.launch { - _commands.emit(InterviewQuizCommand.NavigateBack) - } - } - - private fun onKnownAnswerClick() { - viewModelScopeSafe.launch { - _screenState.updateLoaded { loaded -> - val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) - ?: return@updateLoaded loaded - loaded.copy( - answers = loaded.answers - + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.KNOWN) - ) - } - } - } - - private fun onUnknownAnswerClick() { - viewModelScopeSafe.launch { - _screenState.updateLoaded { loaded -> - val currentQuestion = loaded.questions.getOrNull(loaded.currentQuestionIndex) - ?: return@updateLoaded loaded - loaded.copy( - answers = loaded.answers - + (currentQuestion.id to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN) - ) - } - } - } - - private fun onShowResultClick() { - viewModelScopeSafe.launch(Dispatchers.IO) { - _commands.emit( - InterviewQuizCommand.NavigateToInterviewQuizResultScreen - ) - } - } - - /** Метод-хелпер для обновления состояния Loaded */ - private inline fun MutableStateFlow.updateLoaded( - transform: (InterviewQuizState.Loaded) -> InterviewQuizState.Loaded - ) { - update { state -> - val loaded = state as? InterviewQuizState.Loaded ?: return@update state - transform(loaded) + InterviewQuizEvent.ToDo -> { /* TODO */ } } } /** Создание списка вопросов для тестирования превью */ - private fun previewQuestions(): List { - val base1 = InterviewQuizState.Loaded.VoQuestion(0, "Что такое Virtual DOM, и как он работает?", "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений.") - val base2 = InterviewQuizState.Loaded.VoQuestion(0, "Пример вопроса, на который пользователь должен ответить?", "Пример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответПример ответа, который пользователю должен высветиться, когда будет нажата кнопка Показать ответ, и скрыться, когда будет нажата кнопка Скрыть ответ") - val questions = mutableListOf() + @Suppress("MagicNumber") + private fun previewQuestions(): List { + val shortAnswer = "Виртуальный DOM (VDOM) — это легковесное " + + "представление реального DOM в памяти, которое используется в " + + "JavaScript-библиотеках, таких как React и Vue, " + + "для повышения производительности веб-приложений." + + val base = VoQuestion( + id = 0, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = shortAnswer + ) + val questions = mutableListOf() repeat(10) { index -> - if (index % 2 == 0) { - questions.add(base1.copy(id = index.toLong())) - } else { - questions.add(base2.copy(id = index.toLong())) - } + questions.add(base.copy(id = index.toLong())) } + return questions } + private data class UserInput( + val isAnswerVisible: Boolean, + val answers: Map, + val selectedAnswer: QuizAnswer + ) + companion object { - private const val RESPONSE_DELAY = 2500L private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L + + private const val FIRST_QUESTION_INDEX = 0 } } diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt index a932dbf7..77d93afa 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt @@ -71,37 +71,12 @@ private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp private val FIGMA_CARD_ELEVATION = 4.dp private val FIGMA_RADIUS = 12.dp -private data class QuestionCardState( - val questionText: String, - val shortAnswer: String, - val currentAnswer: InterviewQuizState.Loaded.QuizAnswer, - val isAnswerVisible: Boolean, - val canGoPrevious: Boolean, - val canGoNext: Boolean, - val isLastQuestion: Boolean -) - -private fun InterviewQuizState.Loaded.toQuestionCardState(): QuestionCardState { - val question = questions[currentQuestionIndex] - - return QuestionCardState( - questionText = question.title, - shortAnswer = question.shortAnswer, - currentAnswer = currentAnswer, - isAnswerVisible = isAnswerVisible, - canGoPrevious = canGoPrevious, - canGoNext = canGoNext, - isLastQuestion = currentQuestionIndex == questions.lastIndex - ) -} - @Composable private fun ScreenUI( headerText: TextOrResource, state: InterviewQuizState, onEvent: (InterviewQuizEvent) -> Unit ) { - Scaffold( containerColor = Theme.colors.black10, topBar = { @@ -115,24 +90,12 @@ private fun ScreenUI( when (state) { is InterviewQuizState.Loaded -> BaseQuizScreen( state = state, - onPreviousClick = { - onEvent(InterviewQuizEvent.OnPreviousQuestionClick) - }, - onNextClick = { - onEvent(InterviewQuizEvent.OnNextQuestionClick) - }, - onUnknownClick = { - onEvent(InterviewQuizEvent.OnUnknownAnswerClick) - }, - onKnownClick = { - onEvent(InterviewQuizEvent.OnKnownAnswerClick) - }, - onShowHideAnswerClick = { - onEvent(InterviewQuizEvent.OnShowHideAnswerClick) - }, - onResultClick = { - onEvent(InterviewQuizEvent.OnShowResultClick) - }, + onPreviousClick = { /* TODO */ }, + onNextClick = { /* TODO */ }, + onUnknownClick = { /* TODO */ }, + onKnownClick = { /* TODO */ }, + onShowAnswerClick = { /* TODO */ }, + onResultClick = { /* TODO */ } ) is InterviewQuizState.Error -> ErrorScreen( @@ -157,12 +120,9 @@ private fun BaseQuizScreen( onNextClick: () -> Unit, onUnknownClick: () -> Unit, onKnownClick: () -> Unit, - onShowHideAnswerClick: () -> Unit, - onResultClick: () -> Unit + onShowAnswerClick: () -> Unit, + onResultClick: () -> Unit, ) { - - val cardState = state.toQuestionCardState() - Column( modifier = Modifier .fillMaxSize() @@ -173,17 +133,17 @@ private fun BaseQuizScreen( ) ) { QuizProgress( - current = state.currentQuestionIndex + 1, + current = state.questionIndex + 1, total = state.questionsCount, ) Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)) QuestionCard( - state = cardState, + state = state, onPreviousClick = onPreviousClick, onNextClick = onNextClick, onUnknownClick = onUnknownClick, onKnownClick = onKnownClick, - onShowAnswerClick = onShowHideAnswerClick, + onShowAnswerClick = onShowAnswerClick, onResultClick = onResultClick, ) } @@ -195,7 +155,6 @@ private fun QuizProgress( total: Int, modifier: Modifier = Modifier ) { - val progress = (current.toFloat() / total) DefaultCard(modifier) { @@ -225,7 +184,7 @@ private fun QuizProgress( @Composable private fun QuestionCard( - state: QuestionCardState, + state: InterviewQuizState.Loaded, onPreviousClick: () -> Unit, onNextClick: () -> Unit, onUnknownClick: () -> Unit, @@ -233,24 +192,26 @@ private fun QuestionCard( onShowAnswerClick: () -> Unit, onResultClick: () -> Unit ) { - - /* TODO: нет фичи профиля */ + // TODO: нет фичи профиля var isFavorite by rememberSaveable { mutableStateOf(false) } val favoriteIcon: Painter = painterResource( - if (isFavorite) R.drawable.favorite_filled_icon - else R.drawable.favorite_outlined_icon + if (isFavorite) { + R.drawable.favorite_filled_icon + } else { + R.drawable.favorite_outlined_icon + } ) DefaultCard { - Column(Modifier - .fillMaxWidth() - .padding(FIGMA_MEDIUM_PADDING)) { + Column( + modifier = Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING) + ) { Row(Modifier.fillMaxWidth()) { NavigationButton( text = TextOrResource.Resource(R.string.quiz_btn_prev), - enabled = state.canGoPrevious, + enabled = state.canGoPrev, onClick = onPreviousClick, leadingIcon = painterResource(R.drawable.arrow_left_alt) ) @@ -267,7 +228,6 @@ private fun QuestionCard( .fillMaxWidth() .padding(top = FIGMA_MEDIUM_PADDING), verticalAlignment = Alignment.Top - ) { Icon( painter = painterResource(R.drawable.ellipse_icon), @@ -276,7 +236,7 @@ private fun QuestionCard( tint = Theme.colors.purple800 ) Text( - text = state.questionText, + text = state.question.title, modifier = Modifier .padding(start = FIGMA_LOW_PADDING, end = 12.dp) .weight(1f), @@ -316,7 +276,7 @@ private fun QuestionCard( ) if (state.isAnswerVisible) { Text( - text = state.shortAnswer, + text = state.question.shortAnswer, modifier = Modifier.padding(top = 12.dp), style = Theme.typography.body3 ) @@ -328,14 +288,14 @@ private fun QuestionCard( painter = painterResource(R.drawable.thumbs_down_icon), text = TextOrResource.Resource(R.string.quiz_answer_unknown), onClick = onUnknownClick, - isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN + isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN ) Spacer(Modifier.weight(1f)) QuizAnswerButton( painter = painterResource(R.drawable.thumbs_up_icon), text = TextOrResource.Resource(R.string.quiz_answer_known), onClick = onKnownClick, - isSelected = state.currentAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN + isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN ) } HorizontalDivider( @@ -346,7 +306,7 @@ private fun QuestionCard( PrimaryButton( onClick = onResultClick, modifier = Modifier.height(48.dp), - enabled = state.currentAnswer != InterviewQuizState.Loaded.QuizAnswer.NONE, + enabled = state.selectedAnswer != InterviewQuizState.Loaded.QuizAnswer.NONE, colors = YeahubButtonDefaults.primaryButtonColors( disabledContentColor = Theme.colors.black100 ) @@ -413,7 +373,6 @@ private fun NavigationButton( trailingIcon: Painter? = null, contentPadding: PaddingValues = PaddingValues() ) { - val context = LocalContext.current val color = if (enabled) Theme.colors.purple700 else Theme.colors.purple300 @@ -457,7 +416,6 @@ private fun QuizAnswerButton( onClick: () -> Unit, isSelected: Boolean ) { - val context = LocalContext.current val contentColor = if (isSelected) { @@ -496,29 +454,21 @@ private fun QuizAnswerButton( } } +private val shortAnswerForPreview = "Виртуальный DOM (VDOM) — это легковесное " + + "представление реального DOM в памяти, которое используется в " + + "JavaScript-библиотеках, таких как React и Vue, " + + "для повышения производительности веб-приложений." +private val questionForPreview = InterviewQuizState.Loaded.VoQuestion( + id = 0, + title = "Что такое Virtual DOM, и как он работает?", + shortAnswer = shortAnswerForPreview +) private val questions = listOf( - InterviewQuizState.Loaded.VoQuestion( - id = 0, - title = "Что такое Virtual DOM, и как он работает?", - shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." - ), - InterviewQuizState.Loaded.VoQuestion( - id = 1, - title = "Что такое Virtual DOM, и как он работает?", - shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." - ), - InterviewQuizState.Loaded.VoQuestion( - id = 2, - title = "Что такое Virtual DOM, и как он работает?", - shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." - ), - InterviewQuizState.Loaded.VoQuestion( - id = 3, - title = "Что такое Virtual DOM, и как он работает?", - shortAnswer = "Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти, которое используется в JavaScript-библиотеках, таких как React и Vue, для повышения производительности веб-приложений." - ) + questionForPreview, + questionForPreview.copy(id = 1), + questionForPreview.copy(id = 2), + questionForPreview.copy(id = 3) ) - private val answers = mapOf( 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN, @@ -529,24 +479,39 @@ class QuizScreenStateParamProvider : PreviewParameterProvider = sequenceOf( InterviewQuizState.Loaded( questions = questions, - questionsCount = questions.count(), - currentQuestionIndex = 1, + questionsCount = questions.size, + questionIndex = 0, + question = questions[0], isAnswerVisible = false, - answers = answers + answers = answers, + canGoPrev = false, + canGoNext = false, + selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.NONE, + isLastQuestion = false ), InterviewQuizState.Loaded( questions = questions, - questionsCount = questions.count(), - currentQuestionIndex = 0, + questionsCount = questions.size, + questionIndex = 2, + question = questions[2], isAnswerVisible = true, - answers = answers + answers = answers, + canGoPrev = true, + canGoNext = true, + selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN, + isLastQuestion = false ), InterviewQuizState.Loaded( questions = questions, - questionsCount = questions.count(), - currentQuestionIndex = 3, + questionsCount = questions.size, + questionIndex = 3, + question = questions[3], isAnswerVisible = false, - answers = answers + answers = answers, + canGoPrev = true, + canGoNext = false, + selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.UNKNOWN, + isLastQuestion = true ), InterviewQuizState.Loading, InterviewQuizState.Error( @@ -571,9 +536,8 @@ fun InterviewQuizScreen( @Preview(showBackground = true) @Composable fun DynamicPreviewUI() { - - val mockViewModel = viewModelCreator { - InterviewQuizViewModel(InterviewQuizScreenMapper) + val mockViewModel = previewViewModel { + InterviewQuizViewModel(InterviewQuizScreenMapper()) } val state by mockViewModel.screenState.collectAsState() @@ -585,16 +549,17 @@ fun DynamicPreviewUI() { ) } -typealias ViewModelCreator = () -> ViewModel? +typealias PreviewViewModelCreator = () -> VM -class ViewModelFactory( - private val viewModelCreator: ViewModelCreator = { null }, +private class PreviewViewModelFactory( + private val creator: PreviewViewModelCreator ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = viewModelCreator() as T + override fun create(modelClass: Class): T = creator() as T } @Composable -inline fun viewModelCreator(noinline creator: ViewModelCreator): VM = - viewModel(factory = remember { ViewModelFactory(creator) }) \ No newline at end of file +private inline fun previewViewModel( + noinline creator: PreviewViewModelCreator +): VM = viewModel(factory = remember { PreviewViewModelFactory(creator) }) \ No newline at end of file diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt index 0f086f3b..2a83347c 100644 --- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt +++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt @@ -15,26 +15,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.valentinilk.shimmer.shimmer -import ru.yeahub.core_ui.example.staticPreview.StaticPreview import ru.yeahub.core_ui.theme.Theme @Composable fun InterviewQuizLoading() { - Column(Modifier.padding(horizontal = 16.dp).fillMaxSize()) { PlaceHolderBlock( Modifier.padding(vertical = 24.dp).fillMaxWidth().height(65.dp) ) - PlaceHolderBlock(Modifier.fillMaxWidth().height(320.dp)) - - } } @Composable private fun PlaceHolderBlock(modifier: Modifier = Modifier) { - Card( modifier = modifier.shimmer(), colors = CardDefaults.cardColors(containerColor = Theme.colors.white900), @@ -43,11 +37,4 @@ private fun PlaceHolderBlock(modifier: Modifier = Modifier) { ) { Box(Modifier.fillMaxSize().background(Color.LightGray)) } -} - -@StaticPreview -@Composable -private fun QuizLoadingStaticPreview() { - - InterviewQuizLoading() } \ No newline at end of file From 25b604d9878c5766cbe0654668fc58cd91c13ddc Mon Sep 17 00:00:00 2001 From: Artem_Mih <149761873+PanMobile@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:52:08 +0300 Subject: [PATCH 077/126] =?UTF-8?q?[ANDR-83]=20=D0=94=D0=BE=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20Compose?= =?UTF-8?q?=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ANDR-83: Добавлена kotlinx.collections.immutable библиотека * ANDR-83: TextOrResource.kt теперь помечен как @Immutable * ANDR-83: SkillButton.kt компонент изменен. Убрана полная заливка кнопки при активации/нажатии на нее --- .../ru/yeahub/core_ui/component/SkillButton.kt | 14 ++++++-------- .../ru/yeahub/core_utils/common/TextOrResource.kt | 2 ++ gradle/libs.versions.toml | 4 ++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt index 8294368a..83cb838b 100644 --- a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt +++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt @@ -138,12 +138,9 @@ fun DefaultButton( val onSurfaceClick: () -> Unit = { if (fillButton) { - newContainerColor = if (newContainerColor == defaultColor) { - purple - } else { - defaultColor - } - newContentColor = if (newContentColor == black) { + showBorder = !showBorder + + newContentColor = if (showBorder) { defaultColor } else { black @@ -163,6 +160,7 @@ fun DefaultButton( } val border = when { showBorder && !fillButton && !buttonWithoutBackground -> activeBorder() + fillButton && enabled && activeButton -> activeBorder() fillButton && enabled -> null buttonWithoutBackground -> null else -> defaultsBorder() @@ -171,7 +169,7 @@ fun DefaultButton( if (newContentColor == black && buttonWithoutBackground && activeButton) { purple } else if (newContentColor == black && fillButton && activeButton) { - defaultColor + black } else { newContentColor } @@ -182,7 +180,7 @@ fun DefaultButton( enabled = enabled, shape = shape, color = if (activeButton && fillButton) { - purple + defaultColor } else { if (buttonWithoutBackground) Color.Transparent else newContainerColor }, diff --git a/core/utils/src/main/java/ru/yeahub/core_utils/common/TextOrResource.kt b/core/utils/src/main/java/ru/yeahub/core_utils/common/TextOrResource.kt index be109914..23b55a3f 100644 --- a/core/utils/src/main/java/ru/yeahub/core_utils/common/TextOrResource.kt +++ b/core/utils/src/main/java/ru/yeahub/core_utils/common/TextOrResource.kt @@ -1,7 +1,9 @@ package ru.yeahub.core_utils.common import android.content.Context +import androidx.compose.runtime.Immutable +@Immutable sealed class TextOrResource { data class Text(val text: String) : TextOrResource() data class Resource(val resource: Int) : TextOrResource() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 776d5c21..00f8973c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ jsoup = "1.17.2" shimmer = "1.3.3" uiToolingPreviewAndroid = "1.8.3" runtime = "1.9.0" +immutableCollections = "0.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -66,6 +67,9 @@ koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = #COIL coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +#Immutable Collections +immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutableCollections" } + #Jsoup jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } From 35b92b270178a0cd3e59d099bc5c9ad8c7166aa8 Mon Sep 17 00:00:00 2001 From: xMODDIIx <91845532+xMODDIIx@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:34:57 +0300 Subject: [PATCH 078/126] =?UTF-8?q?ANDR-48:=20Authentication=20=E2=80=94?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B8=20api/impl=20=D0=B8=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Фича регистрации: добавлены модули api и impl * Добавлен модуль authentication (api/impl) в impl сделал резделение на forgot, login, registration. Сверстал экран регистрации * Исправлены замечания ktlint и detekt * Подключил core_ui к модулю. Переиспользовал нужную кнопку и подпраил цвет чекбоксов * Исправил валидацию пароля (regex), вынес строки в ресурсы, почистил зависимости API-модуля --- .idea/gradle.xml | 9 +- feature/authentication/api/.gitignore | 1 + feature/authentication/api/build.gradle.kts | 40 +++ feature/authentication/api/consumer-rules.pro | 0 feature/authentication/api/proguard-rules.pro | 21 ++ .../api/src/main/AndroidManifest.xml | 4 + feature/authentication/impl/.gitignore | 1 + feature/authentication/impl/build.gradle.kts | 71 ++++ .../authentication/impl/consumer-rules.pro | 0 .../authentication/impl/proguard-rules.pro | 21 ++ .../impl/src/main/AndroidManifest.xml | 4 + .../presentation/PasswordValidation.kt | 10 + .../presentation/RegistrationScreen.kt | 320 ++++++++++++++++++ .../presentation/RegistrationUiState.kt | 27 ++ .../presentation/RegistrationViewModel.kt | 80 +++++ .../impl/src/main/res/values/strings.xml | 18 + gradle/libs.versions.toml | 1 + settings.gradle.kts | 3 +- 18 files changed, 627 insertions(+), 4 deletions(-) create mode 100644 feature/authentication/api/.gitignore create mode 100644 feature/authentication/api/build.gradle.kts create mode 100644 feature/authentication/api/consumer-rules.pro create mode 100644 feature/authentication/api/proguard-rules.pro create mode 100644 feature/authentication/api/src/main/AndroidManifest.xml create mode 100644 feature/authentication/impl/.gitignore create mode 100644 feature/authentication/impl/build.gradle.kts create mode 100644 feature/authentication/impl/consumer-rules.pro create mode 100644 feature/authentication/impl/proguard-rules.pro create mode 100644 feature/authentication/impl/src/main/AndroidManifest.xml create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt create mode 100644 feature/authentication/impl/src/main/res/values/strings.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index d6d139c0..7a53b8e1 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -21,6 +21,9 @@ diff --git a/feature/authentication/api/.gitignore b/feature/authentication/api/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/authentication/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/authentication/api/build.gradle.kts b/feature/authentication/api/build.gradle.kts new file mode 100644 index 00000000..156a49db --- /dev/null +++ b/feature/authentication/api/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ru.yeahub.authentication.api" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/feature/authentication/api/consumer-rules.pro b/feature/authentication/api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/authentication/api/proguard-rules.pro b/feature/authentication/api/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/authentication/api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/authentication/api/src/main/AndroidManifest.xml b/feature/authentication/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/authentication/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/authentication/impl/.gitignore b/feature/authentication/impl/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/authentication/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/authentication/impl/build.gradle.kts b/feature/authentication/impl/build.gradle.kts new file mode 100644 index 00000000..2759ccdb --- /dev/null +++ b/feature/authentication/impl/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.yeahub.authentication.impl" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } + testOptions { + unitTests.all { + it.useJUnitPlatform() + it.jvmArgs("-XX:+EnableDynamicAgentLoading") + } + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.compose.material.icons.extended) + implementation(project(":core:ui")) + implementation(project(":core:utils")) + implementation(project(":core:network-api")) + implementation(project(":core:navigation-api")) + implementation(libs.timber) + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.android) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.compose.shimmer) + implementation(libs.androidx.navigation.compose) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + testImplementation(libs.junit.jupiter) + testImplementation(platform(libs.junit.bom)) + testRuntimeOnly(libs.junit.platform.launcher) + testImplementation(libs.mockk) +} \ No newline at end of file diff --git a/feature/authentication/impl/consumer-rules.pro b/feature/authentication/impl/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/authentication/impl/proguard-rules.pro b/feature/authentication/impl/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/authentication/impl/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/authentication/impl/src/main/AndroidManifest.xml b/feature/authentication/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/authentication/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt new file mode 100644 index 00000000..2109da95 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt @@ -0,0 +1,10 @@ +package ru.yeahub.authentication.impl.registration.presentation + +private const val MIN_PASSWORD_LENGTH = 8 + +internal fun isPasswordValid(password: String): Boolean { + val isValid = password.matches( + Regex("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{$MIN_PASSWORD_LENGTH,}$") + ) + return isValid +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt new file mode 100644 index 00000000..ad3652b9 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -0,0 +1,320 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import ru.yeahub.authentication.impl.R +import ru.yeahub.core_ui.component.PrimaryButton +import ru.yeahub.core_ui.theme.Theme + +@Composable +fun RegistrationScreen( + state: RegistrationUiState, + onAction: (RegistrationAction) -> Unit, + onOpenPdPolicy: () -> Unit, + onOpenOffer: () -> Unit +) { + val linkColor = MaterialTheme.colorScheme.primary + + Scaffold { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = stringResource(R.string.registration_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + + FormTextField( + title = stringResource(R.string.nickname_title), + placeholder = stringResource(R.string.nickname_placeholder), + value = state.nickname, + onValueChange = { onAction(RegistrationAction.NicknameChanged(it)) }, + keyboardType = KeyboardType.Ascii + ) + + FormTextField( + title = stringResource(R.string.email_title), + placeholder = stringResource(R.string.email_placeholder), + value = state.email, + onValueChange = { onAction(RegistrationAction.EmailChanged(it)) }, + keyboardType = KeyboardType.Email + ) + + FormPasswordField( + title = stringResource(R.string.password_title), + placeholder = stringResource(R.string.password_placeholder), + value = state.password, + isVisible = state.isPasswordVisible, + onValueChange = { onAction(RegistrationAction.PasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.TogglePasswordVisible) } + ) + + FormPasswordField( + title = stringResource(R.string.confirm_password_title), + placeholder = stringResource(R.string.password_placeholder), + value = state.confirmPassword, + isVisible = state.isConfirmPasswordVisible, + onValueChange = { onAction(RegistrationAction.ConfirmPasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.ToggleConfirmPasswordVisible) } + ) + + ConsentRow( + checked = state.isPdAccepted, + onCheckedChange = { onAction(RegistrationAction.PdAcceptedChanged(it)) }, + text = pdConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "pd") onOpenPdPolicy() + } + ) + + ConsentRow( + checked = state.isOfferAccepted, + onCheckedChange = { onAction(RegistrationAction.OfferAcceptedChanged(it)) }, + text = offerConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "offer") onOpenOffer() + } + ) + + ConsentRow( + checked = state.isMailingAccepted, + onCheckedChange = { onAction(RegistrationAction.MailingAcceptedChanged(it)) }, + text = AnnotatedString(stringResource(R.string.marketing_opt_in_text)), + onLinkClicked = { } + ) + + Spacer(Modifier.height(8.dp)) + + PrimaryButton( + onClick = { onAction(RegistrationAction.SubmitClicked) }, + enabled = state.isSubmitEnabled, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.registration_button)) + } + } + } +} + +@Composable +private fun FormTextField( + title: String, + placeholder: String, + value: String, + onValueChange: (String) -> Unit, + keyboardType: KeyboardType, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = title) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = value, + onValueChange = onValueChange, + placeholder = { + Text( + text = placeholder, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = keyboardType), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } +} + +@Composable +private fun FormPasswordField( + title: String, + placeholder: String, + value: String, + isVisible: Boolean, + onValueChange: (String) -> Unit, + onToggleVisibility: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = title) + + PasswordField( + placeholder = placeholder, + value = value, + isVisible = isVisible, + onValueChange = onValueChange, + onToggleVisibility = onToggleVisibility + ) + } +} + +@Composable +private fun PasswordField( + placeholder: String, + value: String, + isVisible: Boolean, + onValueChange: (String) -> Unit, + onToggleVisibility: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + modifier = modifier.fillMaxWidth(), + value = value, + onValueChange = onValueChange, + placeholder = { + Text( + text = placeholder, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + singleLine = true, + visualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = onToggleVisibility) { + Icon( + imageVector = if (isVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, + contentDescription = null + ) + } + }, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) +} + +@Composable +private fun pdConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { + append(stringResource(R.string.pd_consent_prefix)) + pushStringAnnotation(tag = "pd", annotation = "pd") + pushStyle(SpanStyle(color = linkColor)) + append(stringResource(R.string.pd_consent_link)) + pop() + pop() + append(stringResource(R.string.pd_consent_suffix)) +} + +@Composable +private fun offerConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { + append(stringResource(R.string.offer_consent_prefix)) + pushStringAnnotation(tag = "offer", annotation = "offer") + pushStyle(SpanStyle(color = linkColor)) + append(stringResource(R.string.offer_consent_link)) + pop() + pop() +} + +@Composable +private fun ConsentRow( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + text: AnnotatedString, + onLinkClicked: (tag: String) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + colors = CheckboxDefaults.colors( + checkedColor = Theme.colors.purple700, + uncheckedColor = Theme.colors.purple200, + checkmarkColor = Theme.colors.white900 + ) + ) + Spacer(Modifier.width(8.dp)) + ClickableText( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface, + textDecoration = null + ), + onClick = { offset -> + text.getStringAnnotations(start = offset, end = offset) + .firstOrNull() + ?.let { onLinkClicked(it.tag) } + ?: onCheckedChange(!checked) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun RegistrationScreenPreview() { + MaterialTheme { + RegistrationScreen( + state = RegistrationUiState( + nickname = "admin", + email = "admin@mail.ru", + password = "1234", + confirmPassword = "1234", + isPasswordVisible = true, + isConfirmPasswordVisible = true, + isPdAccepted = true, + isOfferAccepted = true, + isMailingAccepted = false, + isSubmitEnabled = true + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt new file mode 100644 index 00000000..15e96754 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt @@ -0,0 +1,27 @@ +package ru.yeahub.authentication.impl.registration.presentation + +data class RegistrationUiState( + val nickname: String, + val email: String, + val password: String, + val confirmPassword: String, + val isPdAccepted: Boolean, + val isOfferAccepted: Boolean, + val isMailingAccepted: Boolean, + val isPasswordVisible: Boolean, + val isConfirmPasswordVisible: Boolean, + val isSubmitEnabled: Boolean +) + +sealed class RegistrationAction { + data class NicknameChanged(val value: String) : RegistrationAction() + data class EmailChanged(val value: String) : RegistrationAction() + data class PasswordChanged(val value: String) : RegistrationAction() + data class ConfirmPasswordChanged(val value: String) : RegistrationAction() + data class PdAcceptedChanged(val value: Boolean) : RegistrationAction() + data class OfferAcceptedChanged(val value: Boolean) : RegistrationAction() + data class MailingAcceptedChanged(val value: Boolean) : RegistrationAction() + data object TogglePasswordVisible : RegistrationAction() + data object ToggleConfirmPasswordVisible : RegistrationAction() + data object SubmitClicked : RegistrationAction() +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt new file mode 100644 index 00000000..0af91624 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt @@ -0,0 +1,80 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import android.util.Patterns +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class RegistrationViewModel : ViewModel() { + + private val _state = MutableStateFlow( + RegistrationUiState( + nickname = "", + email = "", + password = "", + confirmPassword = "", + isPdAccepted = false, + isOfferAccepted = false, + isMailingAccepted = false, + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isSubmitEnabled = false + ) + ) + val state: StateFlow = _state + + fun onAction(action: RegistrationAction) { + when (action) { + is RegistrationAction.ConfirmPasswordChanged -> { + _state.update { it.copy(confirmPassword = action.value).revalidate() } + } + + is RegistrationAction.EmailChanged -> { + _state.update { it.copy(email = action.value).revalidate() } + } + + is RegistrationAction.MailingAcceptedChanged -> { + _state.update { it.copy(isMailingAccepted = action.value).revalidate() } + } + + is RegistrationAction.NicknameChanged -> { + _state.update { it.copy(nickname = action.value).revalidate() } + } + + is RegistrationAction.OfferAcceptedChanged -> { + _state.update { it.copy(isOfferAccepted = action.value).revalidate() } + } + + is RegistrationAction.PasswordChanged -> { + _state.update { it.copy(password = action.value).revalidate() } + } + + is RegistrationAction.PdAcceptedChanged -> { + _state.update { it.copy(isPdAccepted = action.value).revalidate() } + } + + RegistrationAction.SubmitClicked -> {} + + RegistrationAction.ToggleConfirmPasswordVisible -> { + _state.update { it.copy(isConfirmPasswordVisible = !it.isConfirmPasswordVisible) } + } + + RegistrationAction.TogglePasswordVisible -> { + _state.update { it.copy(isPasswordVisible = !it.isPasswordVisible) } + } + } + } + + private fun RegistrationUiState.revalidate(): RegistrationUiState { + val nicknameOk = nickname.trim().isNotEmpty() + val emailOk = Patterns.EMAIL_ADDRESS.matcher(email.trim()).matches() + val passOk = isPasswordValid(password) + val confirmOk = password == confirmPassword && confirmPassword.isNotEmpty() + val requiredConsentsOk = isPdAccepted && isOfferAccepted + + return copy( + isSubmitEnabled = nicknameOk && emailOk && passOk && confirmOk && requiredConsentsOk + ) + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/res/values/strings.xml b/feature/authentication/impl/src/main/res/values/strings.xml new file mode 100644 index 00000000..85249f30 --- /dev/null +++ b/feature/authentication/impl/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + Регистрация + Никнейм + Введите никнейм + Электронная почта + Введите электронную почту + Пароль + Введите пароль + Подтвердить пароль + Зарегистрироваться + Даю согласие на получение рекламных и информационных рассылок + "Даю " + Согласие на обработку персональных данных + , в соответствии с Политикой в отношении ПД + "Подтверждаю что ознакомился(-ась) с " + Договором-офертой + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00f8973c..25098835 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ runtime = "1.9.0" immutableCollections = "0.4.0" [libraries] +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 616cc767..261ce0c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,4 +57,5 @@ include(":feature:public-collections:api") include(":feature:questions-or-collections") include(":feature:questions-or-collections:api") include(":feature:questions-or-collections:impl") - +include(":feature:authentication:api") +include(":feature:authentication:impl") From 52e9d68d4957e25e3b9717a949faad578fbf4460 Mon Sep 17 00:00:00 2001 From: Artem_Mih <149761873+PanMobile@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:19:46 +0300 Subject: [PATCH 079/126] =?UTF-8?q?ANDR-5:=20=D0=98=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=84?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=20Interview=20Trainer=20=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20Cre?= =?UTF-8?q?ateQuiz=20(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Создан модуль Interview_Trainer * Моделирование состояний первого экрана интервью тренажера - CreateQuiz * Добавление необходимых ресурсов для верстки первого экрана экрана CreateQuiz * Добавление базовых классов фичи * Базовая верстка первого экрана CreateQuizScreen.kt * Добавление объекта интервью тренажера в пути фич для будущей навигации * ANDR-5: VoSpecialization перенесен внутри Loaded * ANDR-5: SkillButton.kt компонент изменен. Убрана полная заливка кнопки при активации/нажатии на нее * ANDR-5: Добавлены дополнительные строковые ресурсы для возможных ошибок * ANDR-5: Создание превью и правки по отступам * ANDR-5: Экран загрузки CreateQuiz * ANDR-5: Использование экрана загрузки в главном для @StaticPreview * ANDR-5: Удалены неиспользуемые строки ошибок * ANDR-5: Убран лишний класс параметров. В StaticPreview передаем сразу стейт * ANDR-5: Убрана преписка Mock у @Composable * ANDR-5: id стало Long по аналогии с другими классами * ANDR-5: Command'ы и Result'ы убраны из папки intents. Сама папка удалена * ANDR-5: Создан интерфейс юзкейса * ANDR-5: Созданы Domain модели * ANDR-5: ВьюМоделька экрана CreateQuiz. Сделана не до конца * ANDR-5: Изменение кол-ва вопросов через данные у ивента * ANDR-5: изменил путь файла * ANDR-5: Изменение кода экрана. Настроено правильное прокидывание лямбд в UI * ANDR-5: Сделано Динамическое превью * ANDR-5: убраны default параметры * ANDR-5: убраны лишние классы * ANDR-5: Создан маппер * ANDR-5: имплементирован маппер и отдельный флоу ввода пользователя для изменения стейта * ANDR-5: сделано рабочее динамик превью * ANDR-5: убран default диспатчер при обновлении ввода пользователя * ANDR-5: убраны параметры по умолчанию в верстке --- .idea/gradle.xml | 3 + .../ru/yeahub/navigation_api/FeatureRoute.kt | 4 + .../interview-trainer/api/build.gradle.kts | 50 ++ .../api/src/main/AndroidManifest.xml | 4 + .../api/InterviewTrainerApi.kt | 21 + .../interview-trainer/impl/build.gradle.kts | 75 +++ .../impl/src/main/AndroidManifest.xml | 4 + .../impl/InterviewTrainerFeatureImpl.kt | 21 + .../presentation/CreateQuizCommand.kt | 10 + .../presentation/CreateQuizEvent.kt | 16 + .../presentation/CreateQuizResult.kt | 10 + .../presentation/CreateQuizScreenMapper.kt | 14 + .../presentation/CreateQuizState.kt | 22 + .../presentation/CreateQuizViewModel.kt | 122 +++++ .../impl/createQuiz/ui/CreateQuizScreen.kt | 438 ++++++++++++++++++ .../createQuiz/ui/CreateQuizScreenLoading.kt | 131 ++++++ .../impl/src/main/res/drawable/minus_icon.xml | 11 + .../impl/src/main/res/drawable/plus_icon.xml | 11 + .../impl/src/main/res/values/strings.xml | 13 + settings.gradle.kts | 4 + 20 files changed, 984 insertions(+) create mode 100644 feature/interview-trainer/api/build.gradle.kts create mode 100644 feature/interview-trainer/api/src/main/AndroidManifest.xml create mode 100644 feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt create mode 100644 feature/interview-trainer/impl/build.gradle.kts create mode 100644 feature/interview-trainer/impl/src/main/AndroidManifest.xml create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml create mode 100644 feature/interview-trainer/impl/src/main/res/values/strings.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7a53b8e1..e00b440c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -39,6 +39,9 @@