diff --git a/.gitignore b/.gitignore
index b3de91f7e0f..9ab78f4efeb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# IntelliJ
*.iml
+/.idea
/.idea/caches
/.idea/libraries
/.idea/modules.xml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 821554dc7ea..44cb024b2c1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,8 +1,11 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.ksp)
+
id("ru.practicum.android.diploma.plugins.developproperties")
}
@@ -11,6 +14,7 @@ android {
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
+
applicationId = "ru.practicum.android.diploma"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
@@ -34,6 +38,7 @@ android {
}
buildFeatures {
buildConfig = true
+ compose = true
}
}
@@ -44,14 +49,59 @@ kotlin {
}
dependencies {
+ // Core
implementation(libs.core.ktx)
implementation(libs.appcompat)
+ implementation(libs.legacy.support.v4)
+ implementation(libs.lifecycle.livedata.ktx)
+ implementation(libs.lifecycle.viewmodel.ktx)
// UI layer libraries
implementation(libs.material)
implementation(libs.constraintlayout)
+ implementation(libs.firebase.crashlytics.buildtools)
+
+ implementation(libs.coroutines.core)
+ implementation(libs.coroutines.android)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.coil)
+ implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0")
+ implementation(libs.coil.svg)
+ implementation(libs.compose.ui)
+ implementation(libs.androidx.viewmodel)
testImplementation(libs.junit4)
+ testImplementation(libs.coroutines.test)
+ androidTestImplementation(libs.junit.ext)
+ androidTestImplementation(libs.espresso.core)
+ implementation(libs.navigation.fragment.ktx)
+ implementation(libs.navigation.ui.ktx)
+ implementation(libs.fragment.ktx)
+ implementation(libs.material.v180)
+
+ implementation(libs.material3)
+ implementation(libs.ui.tooling.preview)
+ debugImplementation(libs.ui.tooling)
+ implementation(libs.activity.compose)
+ implementation(libs.lifecycle.viewmodel.compose)
+
+ // Data layer
+ implementation(libs.retrofit)
+ implementation(libs.retrofit.converter)
+
+ ksp(libs.room.compiler)
+ implementation(libs.room.runtime)
+ implementation(libs.room.ktx)
+
+ // DI
+ implementation(libs.koin)
+
+ // Test
+ testImplementation(libs.junit4)
+ testImplementation(libs.coroutines.test)
+
+ androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.junit.ext)
androidTestImplementation(libs.espresso.core)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 267e2845990..994c3c7a848 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,11 @@
+
+
+
+ android:exported="true"
+ android:screenOrientation="portrait">
@@ -23,4 +29,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/NetworkClient.kt b/app/src/main/java/ru/practicum/android/diploma/data/NetworkClient.kt
new file mode 100644
index 00000000000..a7bcd342520
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/NetworkClient.kt
@@ -0,0 +1,7 @@
+package ru.practicum.android.diploma.data
+
+import ru.practicum.android.diploma.data.dto.Response
+
+interface NetworkClient {
+ suspend fun doRequest(dto: Any): Response
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/UserDataRepositoryImpl.kt b/app/src/main/java/ru/practicum/android/diploma/data/UserDataRepositoryImpl.kt
new file mode 100644
index 00000000000..51fef216720
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/UserDataRepositoryImpl.kt
@@ -0,0 +1,9 @@
+package ru.practicum.android.diploma.data
+
+import ru.practicum.android.diploma.BuildConfig
+import ru.practicum.android.diploma.domain.api.UserDataRepository
+
+class UserDataRepositoryImpl : UserDataRepository {
+
+ override fun getAuthToken(): String = BuildConfig.API_ACCESS_TOKEN
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/VacanciesRepositoryImpl.kt b/app/src/main/java/ru/practicum/android/diploma/data/VacanciesRepositoryImpl.kt
new file mode 100644
index 00000000000..adcc8ded1b7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/VacanciesRepositoryImpl.kt
@@ -0,0 +1,33 @@
+package ru.practicum.android.diploma.data
+
+import ru.practicum.android.diploma.data.dto.VacanciesResponse
+import ru.practicum.android.diploma.data.network.VacanciesRequest
+import ru.practicum.android.diploma.domain.api.VacanciesRepository
+import ru.practicum.android.diploma.domain.models.SearchVacanciesOutcome
+
+class VacanciesRepositoryImpl(
+ private val networkClient: NetworkClient,
+) : VacanciesRepository {
+
+ override suspend fun searchVacancies(searchText: String, page: Int): SearchVacanciesOutcome {
+ val response = networkClient.doRequest(
+ VacanciesRequest(searchText = searchText, page = page),
+ )
+ val data = response.data as? VacanciesResponse
+ return when {
+ response.resultCode != HTTP_OK || data == null -> SearchVacanciesOutcome.Error
+ else -> {
+ val domainResult = data.toDomain()
+ if (domainResult.vacancies.isEmpty()) {
+ SearchVacanciesOutcome.Empty
+ } else {
+ SearchVacanciesOutcome.Success(domainResult)
+ }
+ }
+ }
+ }
+
+ private companion object {
+ const val HTTP_OK = 200
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/VacancyMapper.kt b/app/src/main/java/ru/practicum/android/diploma/data/VacancyMapper.kt
new file mode 100644
index 00000000000..8356db1244a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/VacancyMapper.kt
@@ -0,0 +1,30 @@
+package ru.practicum.android.diploma.data
+
+import ru.practicum.android.diploma.data.dto.VacanciesResponse
+import ru.practicum.android.diploma.data.dto.VacancyCardDto
+import ru.practicum.android.diploma.data.dto.VacancyCardSalaryDto
+import ru.practicum.android.diploma.domain.models.Salary
+import ru.practicum.android.diploma.domain.models.VacanciesSearchResult
+import ru.practicum.android.diploma.domain.models.Vacancy
+
+internal fun VacanciesResponse.toDomain(): VacanciesSearchResult = VacanciesSearchResult(
+ found = found,
+ page = page,
+ pages = pages,
+ vacancies = items.map { it.toDomain() },
+)
+
+private fun VacancyCardDto.toDomain(): Vacancy = Vacancy(
+ id = id,
+ name = name,
+ company = company,
+ city = city,
+ salary = salary?.toDomain(),
+ logo = logo,
+)
+
+private fun VacancyCardSalaryDto.toDomain(): Salary = Salary(
+ from = from,
+ to = to,
+ currency = currency,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/Currency.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/Currency.kt
new file mode 100644
index 00000000000..0732a1377d6
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/Currency.kt
@@ -0,0 +1,15 @@
+package ru.practicum.android.diploma.data.dto
+
+enum class Currency {
+ RUR,
+ RUB,
+ BYR,
+ USD,
+ EUR,
+ KZT,
+ UAH,
+ AZN,
+ UZS,
+ GEL,
+ KGT
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterAreaDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterAreaDto.kt
new file mode 100644
index 00000000000..8f3b05d977d
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterAreaDto.kt
@@ -0,0 +1,8 @@
+package ru.practicum.android.diploma.data.dto
+
+data class FilterAreaDto(
+ val id: Int,
+ val name: String,
+ val parentId: Int?,
+ val areas: List
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterIndustryDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterIndustryDto.kt
new file mode 100644
index 00000000000..42d7a01c412
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/FilterIndustryDto.kt
@@ -0,0 +1,6 @@
+package ru.practicum.android.diploma.data.dto
+
+data class FilterIndustryDto(
+ val id: Int,
+ val name: String
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt
new file mode 100644
index 00000000000..13d168e7c14
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt
@@ -0,0 +1,6 @@
+package ru.practicum.android.diploma.data.dto
+
+data class Response(
+ val data: T?,
+ val resultCode: Int = 0,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacanciesResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacanciesResponse.kt
new file mode 100644
index 00000000000..a6298c9139f
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacanciesResponse.kt
@@ -0,0 +1,8 @@
+package ru.practicum.android.diploma.data.dto
+
+data class VacanciesResponse(
+ val found: Int,
+ val pages: Int,
+ val page: Int,
+ val items: List
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardDto.kt
new file mode 100644
index 00000000000..72b7675852b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardDto.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.data.dto
+
+data class VacancyCardDto(
+ val id: String,
+ val name: String,
+ val company: String?,
+ val city: String?,
+ val salary: VacancyCardSalaryDto?,
+ val logo: String?,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardSalaryDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardSalaryDto.kt
new file mode 100644
index 00000000000..19dc95f65ab
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyCardSalaryDto.kt
@@ -0,0 +1,7 @@
+package ru.practicum.android.diploma.data.dto
+
+data class VacancyCardSalaryDto(
+ val from: Int?,
+ val to: Int?,
+ val currency: String?
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDetailDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDetailDto.kt
new file mode 100644
index 00000000000..e06fec1e03f
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDetailDto.kt
@@ -0,0 +1,65 @@
+package ru.practicum.android.diploma.data.dto
+
+data class VacancyDetailDto(
+ val id: String,
+ val name: String,
+ val description: String,
+ val salary: Salary?,
+ val address: Address?,
+ val experience: Experience?,
+ val schedule: Schedule?,
+ val employment: Employment?,
+ val contacts: Contacts?,
+ val employer: Employer,
+ val area: FilterAreaDto,
+ val skills: List?,
+ val url: String,
+ val industry: FilterIndustryDto
+)
+
+data class Salary(
+ val from: Int?,
+ val to: Int?,
+ val currency: String?
+)
+
+data class Address(
+ val id: String,
+ val city: String,
+ val street: String,
+ val building: String,
+ val raw: String
+)
+
+data class Experience(
+ val id: String,
+ val name: String
+)
+
+data class Schedule(
+ val id: String,
+ val name: String
+)
+
+data class Employment(
+ val id: String,
+ val name: String
+)
+
+data class Contacts(
+ val id: String,
+ val name: String,
+ val email: String,
+ val phones: List
+)
+
+data class Phone(
+ val comment: String?,
+ val formatted: String
+)
+
+data class Employer(
+ val id: String,
+ val name: String,
+ val logo: String
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/ApiService.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/ApiService.kt
new file mode 100644
index 00000000000..fd0435db101
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/ApiService.kt
@@ -0,0 +1,27 @@
+package ru.practicum.android.diploma.data.network
+
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.QueryMap
+import ru.practicum.android.diploma.data.dto.FilterAreaDto
+import ru.practicum.android.diploma.data.dto.FilterIndustryDto
+import ru.practicum.android.diploma.data.dto.VacanciesResponse
+import ru.practicum.android.diploma.data.dto.VacancyDetailDto
+
+interface ApiService {
+ @GET("/areas")
+ suspend fun getAreas(): List
+
+ @GET("/industries")
+ suspend fun getIndustries(): List
+
+ @GET("/vacancies")
+ suspend fun getVacancies(
+ @QueryMap options: Map
+ ): VacanciesResponse
+
+ @GET("/vacancies/{id}")
+ suspend fun getVacancyDetails(
+ @Path("id") id: String
+ ): VacancyDetailDto
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/AreasRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/AreasRequest.kt
new file mode 100644
index 00000000000..431b102e4cc
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/AreasRequest.kt
@@ -0,0 +1,3 @@
+package ru.practicum.android.diploma.data.network
+
+object AreasRequest
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/AuthInterceptor.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/AuthInterceptor.kt
new file mode 100644
index 00000000000..87cdf6d09af
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/AuthInterceptor.kt
@@ -0,0 +1,26 @@
+package ru.practicum.android.diploma.data.network
+
+import android.util.Log
+import okhttp3.Interceptor
+import okhttp3.Response
+import ru.practicum.android.diploma.domain.api.UserDataRepository
+
+class AuthInterceptor(private val userDataRepository: UserDataRepository) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val token = userDataRepository.getAuthToken()
+
+ val requestBuilder = if (token != null) {
+ originalRequest.newBuilder()
+ .header("Authorization", "Bearer $token")
+ } else {
+ Log.e("AuthInterceptor", "Token is null")
+ originalRequest.newBuilder()
+ }
+
+ val request = requestBuilder
+ .header("Content-Type", "application/json")
+ .build()
+ return chain.proceed(request)
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/IndustriesRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/IndustriesRequest.kt
new file mode 100644
index 00000000000..b86b6517899
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/IndustriesRequest.kt
@@ -0,0 +1,3 @@
+package ru.practicum.android.diploma.data.network
+
+object IndustriesRequest
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt
new file mode 100644
index 00000000000..fe5991f0cdb
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt
@@ -0,0 +1,48 @@
+package ru.practicum.android.diploma.data.network
+
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import retrofit2.HttpException
+import ru.practicum.android.diploma.data.NetworkClient
+import ru.practicum.android.diploma.data.dto.Response
+import ru.practicum.android.diploma.util.NetworkConnectionChecker
+import java.io.IOException
+
+class RetrofitNetworkClient(
+ private val apiService: ApiService,
+ private val networkConnectionChecker: NetworkConnectionChecker
+) : NetworkClient {
+
+ override suspend fun doRequest(dto: Any): Response {
+ if (!networkConnectionChecker.isConnected()) {
+ return Response(data = null, resultCode = -1)
+ }
+
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = when (dto) {
+ is VacanciesRequest -> apiService.getVacancies(dto.toMap())
+ is VacancyDetailsRequest -> apiService.getVacancyDetails(dto.id)
+ is AreasRequest -> apiService.getAreas()
+ is IndustriesRequest -> apiService.getIndustries()
+ else -> null
+ }
+ if (response == null) {
+ Response(data = null, resultCode = 400)
+ } else {
+ Response(data = response, resultCode = 200)
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Network request failed", e)
+ Response(data = null, resultCode = -1)
+ } catch (e: HttpException) {
+ Response(data = null, resultCode = e.code())
+ }
+ }
+ }
+
+ private companion object {
+ const val TAG = "RetrofitNetworkClient"
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/VacanciesRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/VacanciesRequest.kt
new file mode 100644
index 00000000000..5259aab6d97
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/VacanciesRequest.kt
@@ -0,0 +1,23 @@
+package ru.practicum.android.diploma.data.network
+
+data class VacanciesRequest(
+ val searchText: String,
+ val area: Int? = null,
+ val industry: Int? = null,
+ val salary: Int? = null,
+ val page: Int = 0,
+ val onlyWithSalary: Boolean = false
+)
+
+fun VacanciesRequest.toMap(): Map {
+ val options = mutableMapOf()
+ options["text"] = searchText
+ options["page"] = page.toString()
+ area?.let { options["area"] = it.toString() }
+ industry?.let { options["industry"] = it.toString() }
+ salary?.let { options["salary"] = it.toString() }
+ if (onlyWithSalary) {
+ options["only_with_salary"] = "true"
+ }
+ return options
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/VacancyDetailsRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/VacancyDetailsRequest.kt
new file mode 100644
index 00000000000..8ed5de5da70
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/data/network/VacancyDetailsRequest.kt
@@ -0,0 +1,3 @@
+package ru.practicum.android.diploma.data.network
+
+data class VacancyDetailsRequest(val id: String)
diff --git a/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt
new file mode 100644
index 00000000000..1a01a2bb511
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt
@@ -0,0 +1,41 @@
+package ru.practicum.android.diploma.di
+
+import com.google.gson.Gson
+import okhttp3.OkHttpClient
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import ru.practicum.android.diploma.data.NetworkClient
+import ru.practicum.android.diploma.data.UserDataRepositoryImpl
+import ru.practicum.android.diploma.data.network.ApiService
+import ru.practicum.android.diploma.data.network.AuthInterceptor
+import ru.practicum.android.diploma.data.network.RetrofitNetworkClient
+import ru.practicum.android.diploma.domain.api.UserDataRepository
+
+val dataModule = module {
+
+ single {
+ OkHttpClient.Builder()
+ .addInterceptor(AuthInterceptor(get()))
+ .build()
+ }
+
+ single {
+ Retrofit.Builder()
+ .baseUrl("https://android-diploma.education-services.ru")
+ .client(get())
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(ApiService::class.java)
+ }
+
+ factory { Gson() }
+
+ single {
+ RetrofitNetworkClient(get(), get())
+ }
+
+ single {
+ UserDataRepositoryImpl()
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt
new file mode 100644
index 00000000000..a03cd17f02b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.di
+
+import org.koin.dsl.module
+import ru.practicum.android.diploma.domain.impl.SearchInteractor
+
+val interactorModule = module {
+ factory {
+ SearchInteractor(get())
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt
new file mode 100644
index 00000000000..322c776959c
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt
@@ -0,0 +1,11 @@
+package ru.practicum.android.diploma.di
+
+import org.koin.dsl.module
+import ru.practicum.android.diploma.data.VacanciesRepositoryImpl
+import ru.practicum.android.diploma.domain.api.VacanciesRepository
+
+val repositoryModule = module {
+ single {
+ VacanciesRepositoryImpl(get())
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/di/UtilsModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/UtilsModule.kt
new file mode 100644
index 00000000000..0a5fb7bd387
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/di/UtilsModule.kt
@@ -0,0 +1,12 @@
+package ru.practicum.android.diploma.di
+
+import org.koin.dsl.module
+import ru.practicum.android.diploma.util.NetworkConnectionChecker
+import ru.practicum.android.diploma.util.NetworkConnectionCheckerImpl
+
+val utilsModule = module {
+
+ single {
+ NetworkConnectionCheckerImpl(get())
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt
new file mode 100644
index 00000000000..a5942781957
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt
@@ -0,0 +1,9 @@
+package ru.practicum.android.diploma.di
+
+import org.koin.core.module.dsl.viewModelOf
+import org.koin.dsl.module
+import ru.practicum.android.diploma.presentation.search.viewmodel.JobSearchViewModel
+
+val viewModelModule = module {
+ viewModelOf(::JobSearchViewModel)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/api/UserDataRepository.kt b/app/src/main/java/ru/practicum/android/diploma/domain/api/UserDataRepository.kt
new file mode 100644
index 00000000000..8ff893ab8ab
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/api/UserDataRepository.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.domain.api
+
+interface UserDataRepository {
+ fun getAuthToken(): String?
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/api/VacanciesRepository.kt b/app/src/main/java/ru/practicum/android/diploma/domain/api/VacanciesRepository.kt
new file mode 100644
index 00000000000..120f26491f5
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/api/VacanciesRepository.kt
@@ -0,0 +1,7 @@
+package ru.practicum.android.diploma.domain.api
+
+import ru.practicum.android.diploma.domain.models.SearchVacanciesOutcome
+
+interface VacanciesRepository {
+ suspend fun searchVacancies(searchText: String, page: Int = 0): SearchVacanciesOutcome
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/impl/SearchInteractor.kt b/app/src/main/java/ru/practicum/android/diploma/domain/impl/SearchInteractor.kt
new file mode 100644
index 00000000000..e58a1d7847a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/impl/SearchInteractor.kt
@@ -0,0 +1,16 @@
+package ru.practicum.android.diploma.domain.impl
+
+import ru.practicum.android.diploma.domain.api.VacanciesRepository
+import ru.practicum.android.diploma.domain.models.SearchVacanciesOutcome
+
+class SearchInteractor(
+ private val vacanciesRepository: VacanciesRepository,
+) {
+ suspend fun searchVacancies(query: String, page: Int = 0): SearchVacanciesOutcome {
+ val normalizedQuery = query.trim().lowercase()
+ if (normalizedQuery.isEmpty()) {
+ return SearchVacanciesOutcome.Error
+ }
+ return vacanciesRepository.searchVacancies(normalizedQuery, page)
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Salary.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Salary.kt
new file mode 100644
index 00000000000..0a111cb5c3a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Salary.kt
@@ -0,0 +1,7 @@
+package ru.practicum.android.diploma.domain.models
+
+data class Salary(
+ val from: Int?,
+ val to: Int?,
+ val currency: String?
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/SearchVacanciesOutcome.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/SearchVacanciesOutcome.kt
new file mode 100644
index 00000000000..ea270062108
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/SearchVacanciesOutcome.kt
@@ -0,0 +1,9 @@
+package ru.practicum.android.diploma.domain.models
+
+sealed interface SearchVacanciesOutcome {
+ data class Success(val result: VacanciesSearchResult) : SearchVacanciesOutcome
+
+ data object Empty : SearchVacanciesOutcome
+
+ data object Error : SearchVacanciesOutcome
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesSearchResult.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesSearchResult.kt
new file mode 100644
index 00000000000..5904c83eaa7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesSearchResult.kt
@@ -0,0 +1,8 @@
+package ru.practicum.android.diploma.domain.models
+
+data class VacanciesSearchResult(
+ val found: Int,
+ val page: Int,
+ val pages: Int,
+ val vacancies: List,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt
new file mode 100644
index 00000000000..bf2a051e63d
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.domain.models
+
+data class Vacancy(
+ val id: String,
+ val name: String,
+ val company: String?,
+ val city: String?,
+ val salary: Salary?,
+ val logo: String?,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/favorites/viewmodel/FavoritesViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/favorites/viewmodel/FavoritesViewModel.kt
new file mode 100644
index 00000000000..43b25f59abd
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/favorites/viewmodel/FavoritesViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.favorites.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class FavoritesViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/country/viewmodel/ChooseCountryViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/country/viewmodel/ChooseCountryViewModel.kt
new file mode 100644
index 00000000000..2cb9c1042e9
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/country/viewmodel/ChooseCountryViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.filtration.country.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class ChooseCountryViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/industry/viewmodel/ChooseIndustryViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/industry/viewmodel/ChooseIndustryViewModel.kt
new file mode 100644
index 00000000000..91b8bbe9a43
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/industry/viewmodel/ChooseIndustryViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.filtration.industry.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class ChooseIndustryViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/region/viewmodel/ChooseRegionViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/region/viewmodel/ChooseRegionViewModel.kt
new file mode 100644
index 00000000000..ce206d2bcd9
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/region/viewmodel/ChooseRegionViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.filtration.region.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class ChooseRegionViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/viewmodel/FiltrationViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/viewmodel/FiltrationViewModel.kt
new file mode 100644
index 00000000000..5c56c593ce6
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/viewmodel/FiltrationViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.filtration.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class FiltrationViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/workplace/viewmodel/PlaceOfWorkViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/workplace/viewmodel/PlaceOfWorkViewModel.kt
new file mode 100644
index 00000000000..986270ef034
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/filtration/workplace/viewmodel/PlaceOfWorkViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.filtration.workplace.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class PlaceOfWorkViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/search/state/JobSearchState.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/search/state/JobSearchState.kt
new file mode 100644
index 00000000000..105f8a75092
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/search/state/JobSearchState.kt
@@ -0,0 +1,17 @@
+package ru.practicum.android.diploma.presentation.search.state
+
+import ru.practicum.android.diploma.domain.models.Vacancy
+
+sealed interface JobSearchState {
+ data object Initial : JobSearchState
+
+ data object Empty : JobSearchState
+
+ data object Error : JobSearchState
+
+ data class Content(
+ val found: Int,
+ val vacancies: List,
+ val isLoading: Boolean = false,
+ ) : JobSearchState
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/search/viewmodel/JobSearchViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/search/viewmodel/JobSearchViewModel.kt
new file mode 100644
index 00000000000..df3fe7b24d2
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/search/viewmodel/JobSearchViewModel.kt
@@ -0,0 +1,151 @@
+package ru.practicum.android.diploma.presentation.search.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import ru.practicum.android.diploma.domain.impl.SearchInteractor
+import ru.practicum.android.diploma.domain.models.SearchVacanciesOutcome
+import ru.practicum.android.diploma.domain.models.Vacancy
+import ru.practicum.android.diploma.presentation.search.state.JobSearchState
+import ru.practicum.android.diploma.util.SEARCH_DEBOUNCE_MS
+
+@OptIn(FlowPreview::class)
+class JobSearchViewModel(
+ private val searchInteractor: SearchInteractor,
+) : ViewModel() {
+
+ private val _searchQuery = MutableStateFlow("")
+ val searchQuery: StateFlow = _searchQuery.asStateFlow()
+
+ private val _state = MutableStateFlow(JobSearchState.Initial)
+ val state: StateFlow = _state.asStateFlow()
+
+ private val _uiState = MutableStateFlow(SearchState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private var currentPage = 0
+ private var maxPages = 0
+
+ init {
+ _searchQuery
+ .debounce(SEARCH_DEBOUNCE_MS)
+ .distinctUntilChanged()
+ .filter { it.isNotBlank() }
+ .onEach { query -> performSearch(query, page = 0) }
+ .launchIn(viewModelScope)
+ }
+
+ fun onSearchQueryChanged(query: String) {
+ _searchQuery.value = query
+ if (query.isBlank()) {
+ resetSearchState()
+ }
+ }
+
+ fun clearSearch() {
+ _searchQuery.value = ""
+ resetSearchState()
+ }
+
+ fun loadNextPage() {
+ val query = _searchQuery.value.trim()
+ val content = _state.value as? JobSearchState.Content ?: return
+ val nextPage = currentPage + 1
+ if (!canLoadNextPage(content, query, nextPage)) {
+ return
+ }
+ viewModelScope.launch {
+ _state.value = content.copy(isLoading = true)
+ when (val outcome = searchInteractor.searchVacancies(query, nextPage)) {
+ is SearchVacanciesOutcome.Success -> {
+ currentPage = outcome.result.page
+ _state.value = JobSearchState.Content(
+ found = outcome.result.found,
+ vacancies = content.vacancies + outcome.result.vacancies,
+ isLoading = false,
+ )
+ }
+ is SearchVacanciesOutcome.Empty,
+ is SearchVacanciesOutcome.Error,
+ -> stopPaginationLoading()
+ }
+ }
+ }
+
+ private fun canLoadNextPage(
+ content: JobSearchState.Content,
+ query: String,
+ nextPage: Int,
+ ): Boolean {
+ if (query.isEmpty()) {
+ return false
+ }
+ return !content.isLoading && nextPage < maxPages
+ }
+
+ private fun stopPaginationLoading() {
+ val currentContent = _state.value as? JobSearchState.Content ?: return
+ _state.value = currentContent.copy(isLoading = false)
+ }
+
+ private fun performSearch(query: String, page: Int) {
+ viewModelScope.launch {
+ resetPagination()
+ _state.value = JobSearchState.Content(
+ found = 0,
+ vacancies = emptyList(),
+ isLoading = true,
+ )
+ when (val outcome = searchInteractor.searchVacancies(query, page)) {
+ is SearchVacanciesOutcome.Success -> applySuccess(outcome, replaceList = true)
+ SearchVacanciesOutcome.Empty -> _state.value = JobSearchState.Empty
+ SearchVacanciesOutcome.Error -> _state.value = JobSearchState.Error
+ }
+ }
+ }
+
+ private fun applySuccess(outcome: SearchVacanciesOutcome.Success, replaceList: Boolean) {
+ currentPage = outcome.result.page
+ maxPages = outcome.result.pages
+ val currentContent = _state.value as? JobSearchState.Content
+ val vacancies = if (replaceList) {
+ outcome.result.vacancies
+ } else {
+ currentContent?.vacancies.orEmpty() + outcome.result.vacancies
+ }
+ _state.value = JobSearchState.Content(
+ found = outcome.result.found,
+ vacancies = vacancies,
+ isLoading = false,
+ )
+ }
+
+ private fun resetPagination() {
+ currentPage = 0
+ maxPages = 0
+ }
+
+ private fun resetSearchState() {
+ resetPagination()
+ _state.value = JobSearchState.Initial
+ }
+
+ data class SearchState(
+ val jobList: List = emptyList(),
+ val selectedJob: Vacancy? = null,
+ val errorVisible: Boolean = false,
+ val recyclerVisible: Boolean = false,
+ val progressBarVisible: Boolean = false,
+ val errorText: String = "",
+ val errorIcon: Int = 0,
+ )
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/team/viewmodel/TeamViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/team/viewmodel/TeamViewModel.kt
new file mode 100644
index 00000000000..b7ee7ba4efd
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/team/viewmodel/TeamViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.team.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class TeamViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/vacancy/viewmodel/VacancyViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/vacancy/viewmodel/VacancyViewModel.kt
new file mode 100644
index 00000000000..164c3330bc1
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/presentation/vacancy/viewmodel/VacancyViewModel.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.presentation.vacancy.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class VacancyViewModel : ViewModel()
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/BadgeItem.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/BadgeItem.kt
new file mode 100644
index 00000000000..ff896ec0a34
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/BadgeItem.kt
@@ -0,0 +1,53 @@
+package ru.practicum.android.diploma.ui.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+@Composable
+fun BadgeItem(modifier: Modifier = Modifier, vacancyAmount: Int) {
+ val text = if (vacancyAmount > 0) {
+ " ${
+ pluralStringResource(
+ id = R.plurals.vacancy_found_count,
+ count = vacancyAmount,
+ vacancyAmount
+ )
+ }"
+ } else {
+ stringResource(R.string.no_vacancies)
+ }
+
+ Text(
+ modifier = modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(color = MaterialTheme.colorScheme.primary)
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ color = MaterialTheme.colorScheme.onPrimary,
+ text = text,
+ style = MaterialTheme.typography.labelMedium,
+ textAlign = TextAlign.Center
+ )
+}
+
+private const val VACANCY_AMOUNT = 395
+
+@Preview
+@Composable
+private fun BadgeItemPreview() {
+ AppTheme {
+ BadgeItem(Modifier, VACANCY_AMOUNT)
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/Loader.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/Loader.kt
new file mode 100644
index 00000000000..4d718ae5e8a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/Loader.kt
@@ -0,0 +1,29 @@
+package ru.practicum.android.diploma.ui.common
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+
+@Composable
+fun Loader(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun LoaderPreview() {
+ Loader()
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/PlaceholderLayout.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/PlaceholderLayout.kt
new file mode 100644
index 00000000000..8b0b695d3a3
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/PlaceholderLayout.kt
@@ -0,0 +1,49 @@
+package ru.practicum.android.diploma.ui.common
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+
+private const val PLACEHOLDER_IMAGE_ASPECT_RATIO = 1.5f
+
+@Composable
+fun PlaceholderLayout(
+ @DrawableRes imageRes: Int,
+ @StringRes textRes: Int? = null
+) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(PLACEHOLDER_IMAGE_ASPECT_RATIO),
+ painter = painterResource(imageRes),
+ contentDescription = null
+ )
+
+ if (textRes != null) {
+ Text(
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Center,
+ text = stringResource(textRes)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/TopBar.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/TopBar.kt
new file mode 100644
index 00000000000..b6c06b9651a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/TopBar.kt
@@ -0,0 +1,79 @@
+package ru.practicum.android.diploma.ui.common
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.ui.theme.Dimens
+
+@Composable
+fun TopBar(
+ text: String,
+ navIconVisible: Boolean,
+ endFirstIconVisible: Boolean,
+ endFirstIconId: Int = R.drawable.ic_share,
+ endSecondIconVisible: Boolean,
+ endSecondIconId: Int = R.drawable.ic_like_empty
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .windowInsetsPadding(WindowInsets.statusBars)
+ .height(64.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (navIconVisible) {
+ IconImage(R.drawable.ic_back)
+ } else {
+ Spacer(modifier = Modifier.width(Dimens.ScreenHorizontalPadding))
+ }
+ Text(
+ text = text,
+ style = MaterialTheme.typography.titleLarge
+ .copy(color = MaterialTheme.colorScheme.onBackground),
+ modifier = Modifier.padding(start = 12.dp),
+ )
+ if (endFirstIconVisible || endSecondIconVisible) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ if (endFirstIconVisible) {
+ IconImage(resId = endFirstIconId)
+ }
+ if (endSecondIconVisible) {
+ IconImage(resId = endSecondIconId)
+ }
+ }
+}
+
+@Composable
+fun IconImage(
+ resId: Int,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .height(48.dp)
+ .width(48.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ painter = painterResource(id = resId),
+ contentDescription = null,
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacanciesContent.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacanciesContent.kt
new file mode 100644
index 00000000000..168583bf6e7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacanciesContent.kt
@@ -0,0 +1,64 @@
+package ru.practicum.android.diploma.ui.common.search
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import ru.practicum.android.diploma.domain.models.Vacancy
+import ru.practicum.android.diploma.ui.common.BadgeItem
+import ru.practicum.android.diploma.ui.common.Loader
+import ru.practicum.android.diploma.ui.mocks.MocData
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+@Composable
+fun VacanciesContent(
+ modifier: Modifier = Modifier,
+ vacancies: List,
+ vacancyAmount: Int,
+ isLoading: Boolean,
+ onVacancyClick: () -> Unit,
+ onLoadNextPage: () -> Unit
+) {
+ Column(modifier = modifier) {
+ Box(contentAlignment = Alignment.TopCenter) {
+ VacancyList(
+ vacancies = vacancies,
+ isLoading = isLoading,
+ onClick = onVacancyClick,
+ onLoadNextPage = onLoadNextPage
+ )
+ if (vacancies.isNotEmpty()) {
+ BadgeItem(
+ modifier = Modifier
+ .padding(top = 12.dp, bottom = 8.dp)
+ .zIndex(1f),
+ vacancyAmount = vacancyAmount
+ )
+ }
+ }
+ if (isLoading && vacancies.isEmpty()) {
+ Loader(modifier.weight(1F).imePadding())
+ }
+ }
+
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun VacanciesContentPreview() {
+ AppTheme {
+ VacanciesContent(
+ vacancies = MocData.vacancies,
+ vacancyAmount = MocData.VACANCY_AMOUNT,
+ isLoading = true,
+ onVacancyClick = {},
+ onLoadNextPage = {}
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyItem.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyItem.kt
new file mode 100644
index 00000000000..44e89f7c1aa
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyItem.kt
@@ -0,0 +1,121 @@
+package ru.practicum.android.diploma.ui.common.search
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import coil3.network.NetworkHeaders
+import coil3.network.httpHeaders
+import coil3.request.ImageRequest
+import coil3.request.crossfade
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.domain.models.Vacancy
+import ru.practicum.android.diploma.ui.mocks.MocData
+import ru.practicum.android.diploma.ui.theme.AppTheme
+import ru.practicum.android.diploma.util.extentions.formatDescription
+import ru.practicum.android.diploma.util.extentions.formatSalary
+
+@Composable
+fun VacancyItem(
+ modifier: Modifier = Modifier,
+ vacancy: Vacancy,
+ onClick: () -> Unit = {}
+) {
+ val vacancyDescription = vacancy.formatDescription()
+ val salary = vacancy.salary.formatSalary()
+ val imageModifier = Modifier
+ .size(48.dp)
+ .clip(RoundedCornerShape(12.dp))
+ val context = LocalContext.current
+ val imageRequest = remember(vacancy.logo) {
+ ImageRequest.Builder(context)
+ .data(vacancy.logo)
+ .crossfade(true)
+ .httpHeaders(
+ NetworkHeaders.Builder()
+ .set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
+ .build()
+ )
+ .build()
+ }
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(
+ onClick = onClick
+ )
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ if (vacancy.logo != null) {
+ AsyncImage(
+ modifier = imageModifier,
+ model = imageRequest,
+ contentDescription = null,
+ placeholder = painterResource(R.drawable.ic_logo_48),
+ contentScale = ContentScale.Fit,
+ error = painterResource(R.drawable.ic_logo_48)
+ )
+ } else {
+ Image(
+ modifier = imageModifier,
+ painter = painterResource(R.drawable.ic_logo_48),
+ contentDescription = null
+ )
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = vacancyDescription,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = vacancy.company ?: stringResource(R.string.no_company),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = salary,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun VacancyItemPreview() {
+ AppTheme {
+ VacancyItem(modifier = Modifier.fillMaxWidth(), MocData.vacancy)
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyList.kt b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyList.kt
new file mode 100644
index 00000000000..914942f6e35
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/common/search/VacancyList.kt
@@ -0,0 +1,84 @@
+package ru.practicum.android.diploma.ui.common.search
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.domain.models.Vacancy
+import ru.practicum.android.diploma.ui.common.Loader
+import ru.practicum.android.diploma.ui.mocks.MocData
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+@Composable
+fun VacancyList(
+ modifier: Modifier = Modifier,
+ vacancies: List,
+ isLoading: Boolean,
+ onClick: () -> Unit,
+ onLoadNextPage: () -> Unit
+) {
+ val listState = rememberLazyListState()
+ val shouldLoadNext = remember {
+ derivedStateOf {
+ val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
+ val totalItemsCount = listState.layoutInfo.totalItemsCount
+
+ lastVisibleItemIndex != null && lastVisibleItemIndex >= totalItemsCount - 1
+ }
+ }
+
+ LaunchedEffect(shouldLoadNext.value) {
+ if (shouldLoadNext.value) {
+ onLoadNextPage()
+ }
+ }
+
+ LazyColumn(
+ modifier = modifier,
+ state = listState,
+ contentPadding = PaddingValues(top = 40.dp)
+ ) {
+ items(
+ items = vacancies,
+ key = { it.id }
+ ) { vacancy ->
+ VacancyItem(
+ modifier = Modifier
+ .fillMaxWidth()
+ .animateItem(),
+ vacancy,
+ onClick = onClick
+ )
+ }
+ item {
+ if (isLoading && vacancies.isNotEmpty()) {
+ Loader(
+ modifier = modifier
+ .heightIn(min = 80.dp)
+ )
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun VacancyListPreview() {
+ AppTheme {
+ VacancyList(
+ vacancies = MocData.vacancies,
+ isLoading = true,
+ onClick = {},
+ onLoadNextPage = {}
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/favorites/fragment/FavoritesFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/favorites/fragment/FavoritesFragment.kt
new file mode 100644
index 00000000000..e1bc027f387
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/favorites/fragment/FavoritesFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.favorites.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.favorites.screen.FavoritesScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class FavoritesFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ FavoritesScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/favorites/screen/FavoritesScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/favorites/screen/FavoritesScreen.kt
new file mode 100644
index 00000000000..d7451a337f8
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/favorites/screen/FavoritesScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.favorites.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun FavoritesScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/fragment/ChooseCountryFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/fragment/ChooseCountryFragment.kt
new file mode 100644
index 00000000000..13d34386a2b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/fragment/ChooseCountryFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.filtration.country.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.filtration.country.screen.ChooseCountryScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class ChooseCountryFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ ChooseCountryScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/screen/ChooseCountryScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/screen/ChooseCountryScreen.kt
new file mode 100644
index 00000000000..7b452ffdbcd
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/country/screen/ChooseCountryScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.filtration.country.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun ChooseCountryScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/fragment/FiltrationFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/fragment/FiltrationFragment.kt
new file mode 100644
index 00000000000..752f8de7ed7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/fragment/FiltrationFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.filtration.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.filtration.screen.FiltrationScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class FiltrationFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ FiltrationScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/fragment/ChooseIndustryFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/fragment/ChooseIndustryFragment.kt
new file mode 100644
index 00000000000..bd31a370890
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/fragment/ChooseIndustryFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.filtration.industry.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.filtration.industry.screen.ChooseIndustryScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class ChooseIndustryFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ ChooseIndustryScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/screen/ChooseIndustryScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/screen/ChooseIndustryScreen.kt
new file mode 100644
index 00000000000..98015b4d4af
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/industry/screen/ChooseIndustryScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.filtration.industry.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun ChooseIndustryScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/fragment/ChooseRegionFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/fragment/ChooseRegionFragment.kt
new file mode 100644
index 00000000000..84eda1170c9
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/fragment/ChooseRegionFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.filtration.region.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.filtration.region.screen.ChooseRegionScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class ChooseRegionFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ ChooseRegionScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/screen/ChooseRegionScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/screen/ChooseRegionScreen.kt
new file mode 100644
index 00000000000..6724554efa3
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/region/screen/ChooseRegionScreen.kt
@@ -0,0 +1,12 @@
+package ru.practicum.android.diploma.ui.filtration.region.screen
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import ru.practicum.android.diploma.R
+
+@Composable
+fun ChooseRegionScreen(modifier: Modifier = Modifier) {
+ Text(stringResource(R.string.team))
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/screen/FiltrationScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/screen/FiltrationScreen.kt
new file mode 100644
index 00000000000..ca07d2257f7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/screen/FiltrationScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.filtration.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun FiltrationScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/fragment/PlaceOfWorkFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/fragment/PlaceOfWorkFragment.kt
new file mode 100644
index 00000000000..1f23375dc67
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/fragment/PlaceOfWorkFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.filtration.workplace.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.filtration.workplace.screen.PlaceOfWorkScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class PlaceOfWorkFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ PlaceOfWorkScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/screen/PlaceOfWorkScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/screen/PlaceOfWorkScreen.kt
new file mode 100644
index 00000000000..9c4c22f8160
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/filtration/workplace/screen/PlaceOfWorkScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.filtration.workplace.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun PlaceOfWorkScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/mocks/MocData.kt b/app/src/main/java/ru/practicum/android/diploma/ui/mocks/MocData.kt
new file mode 100644
index 00000000000..95fc30a34d9
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/mocks/MocData.kt
@@ -0,0 +1,60 @@
+package ru.practicum.android.diploma.ui.mocks
+
+import ru.practicum.android.diploma.domain.models.Salary
+import ru.practicum.android.diploma.domain.models.Vacancy
+
+object MocData {
+ const val SALARY_FROM = 10_000
+ const val SALARY_TO = 20_000
+ const val VACANCY_AMOUNT = 395
+ val vacancy: Vacancy = Vacancy(
+ id = "5",
+ name = "Android-разработчик",
+ company = "Еда",
+ city = "Мoсква",
+ salary = Salary(SALARY_FROM, SALARY_TO, "Р"),
+ logo = null
+ )
+ val vacancies = listOf(
+ Vacancy(
+ id = "1",
+ name = "Senior Android Developer",
+ company = "TechCorp",
+ city = "Москва",
+ salary = Salary(from = 300_000, to = 450_000, currency = "RUB"),
+ logo = null
+ ),
+ Vacancy(
+ id = "2",
+ name = "Junior Kotlin Developer",
+ company = "StartupO",
+ city = "Санкт-Петербург",
+ salary = Salary(from = 70_000, to = 100_000, currency = "RUB"),
+ logo = null
+ ),
+ Vacancy(
+ id = "3",
+ name = "Middle Android Engineer",
+ company = "GlobalFinance",
+ city = null,
+ salary = Salary(from = 3_000, to = 4_500, currency = "USD"),
+ logo = null
+ ),
+ Vacancy(
+ id = "4",
+ name = "QA Automation (Kotlin)",
+ company = "MegaRetail",
+ city = "Казань",
+ salary = null,
+ logo = null
+ ),
+ Vacancy(
+ id = "5",
+ name = "Team Lead Android",
+ company = null,
+ city = "Новосибирск",
+ salary = Salary(from = 500_000, to = null, currency = "RUB"),
+ logo = null
+ )
+ )
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/root/App.kt b/app/src/main/java/ru/practicum/android/diploma/ui/root/App.kt
new file mode 100644
index 00000000000..45808a88f2b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/root/App.kt
@@ -0,0 +1,21 @@
+package ru.practicum.android.diploma.ui.root
+
+import android.app.Application
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
+import ru.practicum.android.diploma.di.dataModule
+import ru.practicum.android.diploma.di.interactorModule
+import ru.practicum.android.diploma.di.repositoryModule
+import ru.practicum.android.diploma.di.utilsModule
+import ru.practicum.android.diploma.di.viewModelModule
+
+class App : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ startKoin {
+ androidContext(this@App)
+ modules(dataModule, repositoryModule, interactorModule, viewModelModule, utilsModule)
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/root/RootActivity.kt b/app/src/main/java/ru/practicum/android/diploma/ui/root/RootActivity.kt
index 1b2630a2421..509ed0fffb9 100644
--- a/app/src/main/java/ru/practicum/android/diploma/ui/root/RootActivity.kt
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/root/RootActivity.kt
@@ -1,21 +1,45 @@
package ru.practicum.android.diploma.ui.root
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.core.view.isVisible
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.setupWithNavController
+import com.google.android.material.bottomnavigation.BottomNavigationView
import ru.practicum.android.diploma.BuildConfig
import ru.practicum.android.diploma.R
class RootActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
setContentView(R.layout.activity_root)
+ val bottomNavigationView = findViewById(R.id.bottomNavigationView)
+
// Пример использования access token для HeadHunter API
networkRequestExample(accessToken = BuildConfig.API_ACCESS_TOKEN)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container_view) as NavHostFragment
+ val navController = navHostFragment.navController
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ bottomNavigationView.setupWithNavController(navController)
+
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ when (destination.id) {
+ R.id.jobSearchFragment, R.id.favoritesFragment, R.id.teamFragment -> {
+ bottomNavigationView.isVisible = true
+ }
+
+ else -> bottomNavigationView.isVisible = false
+ }
+ }
}
private fun networkRequestExample(accessToken: String) {
// ...
}
-
}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/search/fragment/JobSearchFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/search/fragment/JobSearchFragment.kt
new file mode 100644
index 00000000000..dd3c12900f2
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/search/fragment/JobSearchFragment.kt
@@ -0,0 +1,56 @@
+package ru.practicum.android.diploma.ui.search.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.fragment.findNavController
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.presentation.search.viewmodel.JobSearchViewModel
+import ru.practicum.android.diploma.ui.search.screen.JobSearchScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class JobSearchFragment : Fragment() {
+ private val viewModel: JobSearchViewModel by viewModel()
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+
+ setContent {
+ AppTheme {
+ val state = viewModel.state.collectAsStateWithLifecycle()
+ val query = viewModel.searchQuery.collectAsStateWithLifecycle()
+
+ JobSearchScreen(
+ state = state.value,
+ searchQuery = query.value,
+ onSearchTextChange = { viewModel.onSearchQueryChanged(it) },
+ onVacancyClick = {
+ findNavController().navigate(R.id.action_jobSearchFragment_to_vacancyFragment)
+ },
+ onClear = { viewModel.clearSearch() },
+ onLoadNextPage = { viewModel.loadNextPage() },
+ onNetworkError = { showToast(context.getString(R.string.network_error_toast)) }
+ )
+ }
+ }
+ }
+ }
+
+ fun showToast(message: String?) {
+ requireActivity().runOnUiThread {
+ Toast.makeText(requireActivity(), message ?: "Empty message", Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/search/screen/JobSearchScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/search/screen/JobSearchScreen.kt
new file mode 100644
index 00000000000..494a6b83d13
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/search/screen/JobSearchScreen.kt
@@ -0,0 +1,269 @@
+package ru.practicum.android.diploma.ui.search.screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.presentation.search.state.JobSearchState
+import ru.practicum.android.diploma.ui.common.BadgeItem
+import ru.practicum.android.diploma.ui.common.IconImage
+import ru.practicum.android.diploma.ui.common.PlaceholderLayout
+import ru.practicum.android.diploma.ui.common.TopBar
+import ru.practicum.android.diploma.ui.common.search.VacanciesContent
+import ru.practicum.android.diploma.ui.theme.Blue
+import ru.practicum.android.diploma.ui.theme.Dimens
+
+@Composable
+fun JobSearchScreen(
+ state: JobSearchState,
+ searchQuery: String,
+ onVacancyClick: () -> Unit,
+ onSearchTextChange: (String) -> Unit,
+ onClear: () -> Unit,
+ onLoadNextPage: () -> Unit,
+ onNetworkError: () -> Unit
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusRequester = remember { FocusRequester() }
+ val isPreview = LocalInspectionMode.current
+ val interactionSource = remember { MutableInteractionSource() }
+
+ LaunchedEffect(Unit) {
+ if (!isPreview) {
+ focusRequester.requestFocus()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopBar(
+ text = stringResource(R.string.search_screen_title),
+ navIconVisible = false,
+ endFirstIconVisible = true,
+ endFirstIconId = R.drawable.ic_filter,
+ endSecondIconVisible = false
+ )
+ },
+ contentWindowInsets = WindowInsets(bottom = 0),
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ SearchQueryField(
+ searchQuery = searchQuery,
+ focusRequester = focusRequester,
+ interactionSource = interactionSource,
+ onSearchTextChange = onSearchTextChange,
+ onClear = onClear,
+ onKeyboardDone = { keyboardController?.hide() },
+ )
+ Box(modifier = Modifier.weight(1F)) {
+ JobSearchStateContent(
+ state = state,
+ onVacancyClick = onVacancyClick,
+ onLoadNextPage = onLoadNextPage,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun SearchQueryField(
+ searchQuery: String,
+ focusRequester: FocusRequester,
+ interactionSource: MutableInteractionSource,
+ onSearchTextChange: (String) -> Unit,
+ onClear: () -> Unit,
+ onKeyboardDone: () -> Unit,
+) {
+ val fieldShape = RoundedCornerShape(8.dp)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = Dimens.ScreenHorizontalPadding,
+ top = 8.dp,
+ end = Dimens.ScreenHorizontalPadding,
+ )
+ .height(56.dp)
+ .clip(fieldShape)
+ .background(MaterialTheme.colorScheme.surfaceContainer),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicTextField(
+ value = searchQuery,
+ onValueChange = onSearchTextChange,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight()
+ .padding(start = 20.dp)
+ .focusRequester(focusRequester),
+ singleLine = true,
+ textStyle = MaterialTheme.typography.bodyMedium
+ .copy(color = MaterialTheme.colorScheme.onBackground),
+ cursorBrush = SolidColor(Blue),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(onDone = { onKeyboardDone() }),
+ interactionSource = interactionSource,
+ decorationBox = { innerTextField ->
+ Box(
+ Modifier.fillMaxSize(),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ if (searchQuery.isEmpty()) {
+ Text(
+ text = stringResource(R.string.search_input_hint),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.inverseOnSurface
+ ),
+ maxLines = 1,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+ innerTextField()
+ }
+ }
+ )
+ IconImage(
+ modifier = Modifier
+ .padding(end = 4.dp)
+ .clickable(enabled = true, onClick = onClear),
+ resId = if (searchQuery.isEmpty()) R.drawable.ic_search else R.drawable.ic_cross,
+ )
+ }
+}
+
+@Composable
+private fun JobSearchStateContent(
+ state: JobSearchState,
+ onVacancyClick: () -> Unit,
+ onLoadNextPage: () -> Unit,
+) {
+ when (state) {
+ is JobSearchState.Content -> VacanciesContent(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ start = Dimens.ScreenHorizontalPadding,
+ top = 8.dp,
+ end = Dimens.ScreenHorizontalPadding,
+ ),
+ vacancies = state.vacancies,
+ vacancyAmount = state.found,
+ isLoading = state.isLoading,
+ onVacancyClick = onVacancyClick,
+ onLoadNextPage = onLoadNextPage
+ )
+
+ JobSearchState.Initial -> {
+ PlaceholderLayout(R.drawable.img_search_initial_placeholder)
+ }
+
+ JobSearchState.Empty -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ BadgeItem(
+ modifier = Modifier.padding(top = 12.dp),
+ vacancyAmount = 0
+ )
+ PlaceholderLayout(
+ R.drawable.img_nothing_found,
+ R.string.no_vacancies_error
+ )
+ }
+ }
+
+ JobSearchState.Error -> {
+ PlaceholderLayout(
+ R.drawable.img_no_internet,
+ R.string.no_internet_error
+ )
+ }
+ }
+}
+
+@Composable
+fun TextField(
+ searchQuery: String,
+ onSearchTextChange: (String) -> Unit,
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusRequester = remember { FocusRequester() }
+ val interactionSource = remember { MutableInteractionSource() }
+
+ return BasicTextField(
+ value = searchQuery,
+ onValueChange = { newText ->
+ onSearchTextChange(newText)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .padding(start = 20.dp)
+ .focusRequester(focusRequester),
+ singleLine = true,
+ textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
+ cursorBrush = SolidColor(Blue),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ keyboardController?.hide()
+ }),
+ interactionSource = interactionSource,
+ decorationBox = { innerTextField ->
+ Box(
+ Modifier.fillMaxSize(), contentAlignment = Alignment.CenterStart
+ ) {
+ if (searchQuery.isEmpty()) {
+ Text(
+ text = stringResource(R.string.search_input_hint),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.inverseOnSurface
+ ),
+ maxLines = 1,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+ innerTextField()
+ }
+ })
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/TeamMembers.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/TeamMembers.kt
new file mode 100644
index 00000000000..3f3429b679f
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/TeamMembers.kt
@@ -0,0 +1,45 @@
+package ru.practicum.android.diploma.ui.team
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.ui.team.model.TeamMember
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+
+@Composable
+fun rememberTeamMembers(): List = listOf(
+ TeamMember(
+ name = stringResource(R.string.team_member_1_name),
+ avatarResId = R.drawable.team_avatar_inna,
+ role = stringResource(R.string.team_member_1_role),
+ roleIconResId = R.drawable.ic_role_captain,
+ roleColor = TeamColors.CaptainRole,
+ roleBackgroundColor = TeamColors.CaptainRoleBackground,
+ ),
+ TeamMember(
+ name = stringResource(R.string.team_member_2_name),
+ avatarResId = R.drawable.team_avatar_maria,
+ role = stringResource(R.string.team_member_2_role),
+ roleIconResId = R.drawable.ic_role_mate,
+ roleColor = TeamColors.MateRole,
+ roleBackgroundColor = TeamColors.MateRoleBackground,
+ avatarContentScale = ContentScale.Fit,
+ ),
+ TeamMember(
+ name = stringResource(R.string.team_member_3_name),
+ avatarResId = R.drawable.team_avatar_denis,
+ role = stringResource(R.string.team_member_3_role),
+ roleIconResId = R.drawable.ic_role_boatswain,
+ roleColor = TeamColors.BoatswainRole,
+ roleBackgroundColor = TeamColors.BoatswainRoleBackground,
+ ),
+ TeamMember(
+ name = stringResource(R.string.team_member_4_name),
+ avatarResId = R.drawable.team_avatar_timofey,
+ role = stringResource(R.string.team_member_4_role),
+ roleIconResId = R.drawable.ic_role_cook,
+ roleColor = TeamColors.CookRole,
+ roleBackgroundColor = TeamColors.CookRoleBackground,
+ ),
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/component/RoleBadge.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/RoleBadge.kt
new file mode 100644
index 00000000000..5c2ab1e8839
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/RoleBadge.kt
@@ -0,0 +1,53 @@
+package ru.practicum.android.diploma.ui.team.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.ui.team.theme.TeamTypography
+
+@Composable
+fun RoleBadge(
+ role: String,
+ roleIconResId: Int,
+ roleColor: Color,
+ roleBackgroundColor: Color,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .height(38.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(roleBackgroundColor)
+ .padding(horizontal = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(roleIconResId),
+ contentDescription = null,
+ tint = roleColor,
+ )
+ Text(
+ text = role,
+ style = TeamTypography.Role,
+ color = roleColor,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamBottomNavigation.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamBottomNavigation.kt
new file mode 100644
index 00000000000..0bac507859b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamBottomNavigation.kt
@@ -0,0 +1,51 @@
+package ru.practicum.android.diploma.ui.team.component
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import ru.practicum.android.diploma.ui.team.model.TeamBottomTab
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+import ru.practicum.android.diploma.ui.theme.Regular12
+
+@Composable
+fun TeamBottomNavigation(
+ selectedTab: TeamBottomTab,
+ onTabSelected: (TeamBottomTab) -> Unit,
+) {
+ NavigationBar(
+ containerColor = TeamColors.CardBackground,
+ ) {
+ TeamBottomTab.entries.forEach { tab ->
+ val selected = tab == selectedTab
+ NavigationBarItem(
+ selected = selected,
+ onClick = { onTabSelected(tab) },
+ icon = {
+ Icon(
+ painter = painterResource(tab.iconResId),
+ contentDescription = stringResource(tab.titleResId),
+ )
+ },
+ label = {
+ Text(
+ text = stringResource(tab.titleResId),
+ style = Regular12,
+ )
+ },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = TeamColors.Accent,
+ selectedTextColor = TeamColors.Accent,
+ unselectedIconColor = TeamColors.BottomNavInactive,
+ unselectedTextColor = TeamColors.BottomNavInactive,
+ indicatorColor = Color.Transparent,
+ ),
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamHeader.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamHeader.kt
new file mode 100644
index 00000000000..a3990ebd9e2
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamHeader.kt
@@ -0,0 +1,36 @@
+package ru.practicum.android.diploma.ui.team.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+import ru.practicum.android.diploma.ui.team.theme.TeamTypography
+
+@Composable
+fun TeamHeader(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, bottom = 20.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.team),
+ style = TeamTypography.ScreenTitle,
+ color = TeamColors.PrimaryBlue,
+ )
+ Text(
+ modifier = Modifier.padding(top = 12.dp),
+ text = stringResource(R.string.team_screen_headline),
+ style = TeamTypography.MainTitle,
+ color = TeamColors.PrimaryText,
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamInfoCard.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamInfoCard.kt
new file mode 100644
index 00000000000..95777b83749
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamInfoCard.kt
@@ -0,0 +1,34 @@
+package ru.practicum.android.diploma.ui.team.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+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.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+import ru.practicum.android.diploma.ui.team.theme.TeamTypography
+
+@Composable
+fun TeamInfoCard(
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(18.dp),
+ colors = CardDefaults.cardColors(containerColor = TeamColors.InfoCardBackground),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp),
+ text = stringResource(R.string.team_footer_tagline),
+ style = TeamTypography.FooterTagline,
+ color = TeamColors.PrimaryBlue,
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamMemberCard.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamMemberCard.kt
new file mode 100644
index 00000000000..a887b26a6c7
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/component/TeamMemberCard.kt
@@ -0,0 +1,99 @@
+package ru.practicum.android.diploma.ui.team.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+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.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.ui.team.model.TeamMember
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+import ru.practicum.android.diploma.ui.team.theme.TeamTypography
+
+@Composable
+fun TeamMemberCard(
+ member: TeamMember,
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(96.dp)
+ .shadow(
+ elevation = 10.dp,
+ shape = RoundedCornerShape(22.dp),
+ spotColor = TeamColors.Accent.copy(alpha = 0.12f),
+ ambientColor = TeamColors.Accent.copy(alpha = 0.08f),
+ ),
+ shape = RoundedCornerShape(22.dp),
+ colors = CardDefaults.cardColors(containerColor = TeamColors.CardBackground),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ TeamMemberAvatar(member = member)
+ Text(
+ modifier = Modifier.weight(1f),
+ text = member.name,
+ style = TeamTypography.MemberName,
+ color = TeamColors.PrimaryText,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ RoleBadge(
+ role = member.role,
+ roleIconResId = member.roleIconResId,
+ roleColor = member.roleColor,
+ roleBackgroundColor = member.roleBackgroundColor,
+ )
+ }
+ }
+}
+
+@Composable
+private fun TeamMemberAvatar(
+ member: TeamMember,
+ modifier: Modifier = Modifier,
+) {
+ val avatarPadding = if (member.avatarContentScale == ContentScale.Fit) 4.dp else 0.dp
+
+ Box(
+ modifier = modifier
+ .size(64.dp)
+ .clip(CircleShape)
+ .background(TeamColors.AvatarBackground),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(avatarPadding),
+ painter = painterResource(member.avatarResId),
+ contentDescription = member.name,
+ contentScale = member.avatarContentScale,
+ )
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/fragment/TeamFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/fragment/TeamFragment.kt
new file mode 100644
index 00000000000..e3489b3b65a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/fragment/TeamFragment.kt
@@ -0,0 +1,29 @@
+package ru.practicum.android.diploma.ui.team.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.team.screen.TeamScreen
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+class TeamFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ // Нижняя навигация уже есть в RootActivity — не дублируем её в Compose.
+ TeamScreen(showBottomNavigation = false)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamBottomTab.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamBottomTab.kt
new file mode 100644
index 00000000000..7782211d721
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamBottomTab.kt
@@ -0,0 +1,14 @@
+package ru.practicum.android.diploma.ui.team.model
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import ru.practicum.android.diploma.R
+
+enum class TeamBottomTab(
+ @StringRes val titleResId: Int,
+ @DrawableRes val iconResId: Int,
+) {
+ Main(R.string.main, R.drawable.ic_main_24),
+ Favorites(R.string.favorites, R.drawable.ic_favorites_24),
+ Team(R.string.team, R.drawable.ic_team_24),
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamMember.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamMember.kt
new file mode 100644
index 00000000000..777bc067baa
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/model/TeamMember.kt
@@ -0,0 +1,15 @@
+package ru.practicum.android.diploma.ui.team.model
+
+import androidx.annotation.DrawableRes
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+
+data class TeamMember(
+ val name: String,
+ @DrawableRes val avatarResId: Int,
+ val role: String,
+ @DrawableRes val roleIconResId: Int,
+ val roleColor: Color,
+ val roleBackgroundColor: Color,
+ val avatarContentScale: ContentScale = ContentScale.Crop,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/screen/TeamScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/screen/TeamScreen.kt
new file mode 100644
index 00000000000..1c366e4e989
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/screen/TeamScreen.kt
@@ -0,0 +1,111 @@
+package ru.practicum.android.diploma.ui.team.screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import ru.practicum.android.diploma.ui.team.component.TeamBottomNavigation
+import ru.practicum.android.diploma.ui.team.component.TeamHeader
+import ru.practicum.android.diploma.ui.team.component.TeamInfoCard
+import ru.practicum.android.diploma.ui.team.component.TeamMemberCard
+import ru.practicum.android.diploma.ui.team.model.TeamBottomTab
+import ru.practicum.android.diploma.ui.team.model.TeamMember
+import ru.practicum.android.diploma.ui.team.rememberTeamMembers
+import ru.practicum.android.diploma.ui.team.theme.TeamColors
+import ru.practicum.android.diploma.ui.theme.AppTheme
+
+@Composable
+fun TeamScreen(
+ showBottomNavigation: Boolean = true,
+ modifier: Modifier = Modifier,
+) {
+ val members = rememberTeamMembers()
+ var selectedTab by remember { mutableStateOf(TeamBottomTab.Team) }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ containerColor = TeamColors.ScreenBackgroundSolid,
+ bottomBar = {
+ if (showBottomNavigation) {
+ TeamBottomNavigation(
+ selectedTab = selectedTab,
+ onTabSelected = { selectedTab = it },
+ )
+ }
+ },
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(TeamColors.screenBackgroundBrush)
+ .padding(innerPadding),
+ ) {
+ TeamScreenContent(members = members)
+ }
+ }
+}
+
+@Composable
+fun TeamScreenContent(
+ members: List,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ contentPadding = PaddingValues(bottom = 24.dp),
+ ) {
+ item(key = "header") {
+ TeamHeader()
+ }
+ items(
+ items = members,
+ key = { it.name },
+ ) { member ->
+ TeamMemberCard(
+ modifier = Modifier.padding(bottom = 12.dp),
+ member = member,
+ )
+ }
+ item(key = "footer_tagline") {
+ TeamInfoCard(
+ modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, showSystemUi = true)
+@Composable
+private fun TeamScreenPreview() {
+ AppTheme {
+ TeamScreen()
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360, heightDp = 800)
+@Composable
+private fun TeamScreenContentPreview() {
+ AppTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(TeamColors.screenBackgroundBrush),
+ ) {
+ TeamScreenContent(members = rememberTeamMembers())
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamColors.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamColors.kt
new file mode 100644
index 00000000000..08627b5961b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamColors.kt
@@ -0,0 +1,52 @@
+package ru.practicum.android.diploma.ui.team.theme
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+
+private const val HEX_SCREEN_BACKGROUND_TOP = 0xFFFAFAFF
+private const val HEX_SCREEN_BACKGROUND_BOTTOM = 0xFFF3F0FF
+private const val HEX_SCREEN_BACKGROUND_SOLID = 0xFFF8F7FF
+private const val HEX_PRIMARY_TEXT = 0xFF1F2937
+private const val HEX_SECONDARY_TEXT = 0xFF6B7280
+private const val HEX_ACCENT = 0xFF6C63FF
+private const val HEX_PRIMARY_BLUE = 0xFF3772E7
+private const val HEX_AVATAR_BACKGROUND = 0xFFF3F4F6
+private const val HEX_CARD_BACKGROUND = 0xFFFFFFFF
+private const val HEX_INFO_CARD_BACKGROUND = 0xFFF1EEFF
+private const val HEX_CAPTAIN_ROLE = 0xFF7C3AED
+private const val HEX_CAPTAIN_ROLE_BACKGROUND = 0xFFEDE9FE
+private const val HEX_MATE_ROLE = 0xFF2563EB
+private const val HEX_MATE_ROLE_BACKGROUND = 0xFFDBEAFE
+private const val HEX_BOATSWAIN_ROLE = 0xFF16A34A
+private const val HEX_BOATSWAIN_ROLE_BACKGROUND = 0xFFDCFCE7
+private const val HEX_COOK_ROLE = 0xFFEA580C
+private const val HEX_COOK_ROLE_BACKGROUND = 0xFFFFEDD5
+private const val HEX_BOTTOM_NAV_INACTIVE = 0xFF9CA3AF
+
+object TeamColors {
+ val ScreenBackgroundTop = Color(HEX_SCREEN_BACKGROUND_TOP)
+ val ScreenBackgroundBottom = Color(HEX_SCREEN_BACKGROUND_BOTTOM)
+ val ScreenBackgroundSolid = Color(HEX_SCREEN_BACKGROUND_SOLID)
+ val PrimaryText = Color(HEX_PRIMARY_TEXT)
+ val SecondaryText = Color(HEX_SECONDARY_TEXT)
+ val Accent = Color(HEX_ACCENT)
+ val PrimaryBlue = Color(HEX_PRIMARY_BLUE)
+ val AvatarBackground = Color(HEX_AVATAR_BACKGROUND)
+ val CardBackground = Color(HEX_CARD_BACKGROUND)
+ val InfoCardBackground = Color(HEX_INFO_CARD_BACKGROUND)
+
+ val CaptainRole = Color(HEX_CAPTAIN_ROLE)
+ val CaptainRoleBackground = Color(HEX_CAPTAIN_ROLE_BACKGROUND)
+ val MateRole = Color(HEX_MATE_ROLE)
+ val MateRoleBackground = Color(HEX_MATE_ROLE_BACKGROUND)
+ val BoatswainRole = Color(HEX_BOATSWAIN_ROLE)
+ val BoatswainRoleBackground = Color(HEX_BOATSWAIN_ROLE_BACKGROUND)
+ val CookRole = Color(HEX_COOK_ROLE)
+ val CookRoleBackground = Color(HEX_COOK_ROLE_BACKGROUND)
+
+ val BottomNavInactive = Color(HEX_BOTTOM_NAV_INACTIVE)
+
+ val screenBackgroundBrush: Brush = Brush.verticalGradient(
+ colors = listOf(ScreenBackgroundTop, ScreenBackgroundBottom),
+ )
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamTypography.kt b/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamTypography.kt
new file mode 100644
index 00000000000..9737db20285
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/team/theme/TeamTypography.kt
@@ -0,0 +1,51 @@
+package ru.practicum.android.diploma.ui.team.theme
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import ru.practicum.android.diploma.ui.theme.AppFontFamily
+
+object TeamTypography {
+ val ScreenTitle = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ lineHeight = 28.sp,
+ )
+ val MainTitle = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 34.sp,
+ lineHeight = 40.sp,
+ )
+ val MemberName = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 17.sp,
+ lineHeight = 22.sp,
+ )
+ val Role = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 18.sp,
+ )
+ val InfoTitle = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
+ lineHeight = 22.sp,
+ )
+ val InfoSubtitle = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 13.sp,
+ lineHeight = 18.sp,
+ )
+ val FooterTagline = TextStyle(
+ fontFamily = AppFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
+ lineHeight = 22.sp,
+ )
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/theme/Color.kt b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Color.kt
new file mode 100644
index 00000000000..f0eb217863a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Color.kt
@@ -0,0 +1,47 @@
+package ru.practicum.android.diploma.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+private const val HEX_BLACK = 0xFF1A1B22
+private const val HEX_WHITE = 0xFFFDFDFD
+private const val HEX_BLUE = 0xFF3772E7
+private const val HEX_RED = 0xFFF56B6C
+private const val HEX_GREY = 0xFFAEAFB4
+private const val HEX_LIGHT_GREY = 0xFFE6E8EB
+private const val BACKGROUND_OVERLAY_ALPHA = 0.5f
+
+val Black = Color(HEX_BLACK)
+val White = Color(HEX_WHITE)
+val Blue = Color(HEX_BLUE)
+val Red = Color(HEX_RED)
+val Grey = Color(HEX_GREY)
+val LightGrey = Color(HEX_LIGHT_GREY)
+val Background = Color(HEX_BLACK).copy(alpha = BACKGROUND_OVERLAY_ALPHA)
+
+val LightPrimary = Blue
+val LightOnPrimary = White
+val LightSecondary = Grey
+val LightOnSecondary = Black
+val LightBackground = White
+val LightOnBackground = Black
+val LightSurface = White
+val LightOnSurface = Black
+val LightSurfaceVariant = LightGrey
+val LightOnSurfaceVariant = Grey
+val LightError = Red
+val LightSurfaceContainer = LightGrey
+val LightInversionOnSurface = Grey
+
+val DarkPrimary = Blue
+val DarkOnPrimary = White
+val DarkSecondary = Grey
+val DarkOnSecondary = Black
+val DarkBackground = Black
+val DarkOnBackground = White
+val DarkSurface = Black
+val DarkOnSurface = White
+val DarkSurfaceVariant = LightGrey
+val DarkOnSurfaceVariant = Grey
+val DarkError = Red
+val DarkSurfaceContainer = Grey
+val DarkInversionOnSurface = White
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/theme/Dimens.kt b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Dimens.kt
new file mode 100644
index 00000000000..a85ec5bd3c4
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Dimens.kt
@@ -0,0 +1,7 @@
+package ru.practicum.android.diploma.ui.theme
+
+import androidx.compose.ui.unit.dp
+
+object Dimens {
+ val ScreenHorizontalPadding = 16.dp
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/theme/Theme.kt b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Theme.kt
new file mode 100644
index 00000000000..3085adc7c9b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Theme.kt
@@ -0,0 +1,45 @@
+package ru.practicum.android.diploma.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+
+@Composable
+fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+ val colors = if (darkTheme) DarkColors else LightColors
+ MaterialTheme(colorScheme = colors, typography = AppTypography, content = content)
+}
+
+private val LightColors = lightColorScheme(
+ primary = LightPrimary,
+ onPrimary = LightOnPrimary,
+ secondary = LightSecondary,
+ onSecondary = LightOnSecondary,
+ background = LightBackground,
+ onBackground = LightOnBackground,
+ surface = LightSurface,
+ onSurface = LightOnSurface,
+ surfaceVariant = LightSurfaceVariant,
+ onSurfaceVariant = LightOnSurfaceVariant,
+ error = LightError,
+ surfaceContainer = LightSurfaceContainer,
+ inverseOnSurface = LightInversionOnSurface
+)
+
+private val DarkColors = darkColorScheme(
+ primary = DarkPrimary,
+ onPrimary = DarkOnPrimary,
+ secondary = DarkSecondary,
+ onSecondary = DarkOnSecondary,
+ background = DarkBackground,
+ onBackground = DarkOnBackground,
+ surface = DarkSurface,
+ onSurface = DarkOnSurface,
+ surfaceVariant = DarkSurfaceVariant,
+ onSurfaceVariant = DarkOnSurfaceVariant,
+ error = DarkError,
+ surfaceContainer = DarkSurfaceContainer,
+ inverseOnSurface = DarkInversionOnSurface
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/theme/Type.kt b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Type.kt
new file mode 100644
index 00000000000..95c2099b74c
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/theme/Type.kt
@@ -0,0 +1,62 @@
+package ru.practicum.android.diploma.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import ru.practicum.android.diploma.R
+
+val AppFontFamily = FontFamily(
+ Font(R.font.ys_display_bold, FontWeight.W700),
+ Font(R.font.ys_display_medium, FontWeight.W500),
+ Font(R.font.ys_display_regular, FontWeight.W400)
+)
+val Bold32 = TextStyle(
+ fontSize = 32.sp,
+ fontFamily = AppFontFamily,
+ letterSpacing = 0.sp,
+ lineHeight = 38.sp,
+ fontWeight = FontWeight.W700
+)
+val Medium22 = TextStyle(
+ fontSize = 22.sp,
+ fontFamily = AppFontFamily,
+ letterSpacing = 0.sp,
+ lineHeight = 26.sp,
+ fontWeight = FontWeight.W500
+)
+val Medium16 = TextStyle(
+ fontSize = 16.sp,
+ fontFamily = AppFontFamily,
+ letterSpacing = 0.sp,
+ lineHeight = 19.sp,
+ fontWeight = FontWeight.W500
+)
+val Regular16 = TextStyle(
+ fontSize = 16.sp,
+ fontFamily = AppFontFamily,
+ letterSpacing = 0.sp,
+ lineHeight = 19.sp,
+ fontWeight = FontWeight.W400
+)
+val Regular12 = TextStyle(
+ fontSize = 12.sp,
+ fontFamily = AppFontFamily,
+ letterSpacing = 0.sp,
+ lineHeight = 16.sp,
+ fontWeight = FontWeight.W400
+)
+
+val AppTypography = Typography(
+ headlineLarge = Bold32,
+ headlineMedium = Medium22,
+ titleLarge = Medium22,
+ titleMedium = Regular16,
+ bodyMedium = Regular16,
+ bodySmall = Regular12,
+ labelLarge = Medium16,
+ labelMedium = Regular16,
+ labelSmall = Regular12,
+)
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/fragment/VacancyFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/fragment/VacancyFragment.kt
new file mode 100644
index 00000000000..73acbb29752
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/fragment/VacancyFragment.kt
@@ -0,0 +1,28 @@
+package ru.practicum.android.diploma.ui.vacancy.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import ru.practicum.android.diploma.ui.theme.AppTheme
+import ru.practicum.android.diploma.ui.vacancy.screen.VacancyScreen
+
+class VacancyFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AppTheme {
+ VacancyScreen()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/screen/VacancyScreen.kt b/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/screen/VacancyScreen.kt
new file mode 100644
index 00000000000..ca70eaefaa9
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/ui/vacancy/screen/VacancyScreen.kt
@@ -0,0 +1,10 @@
+package ru.practicum.android.diploma.ui.vacancy.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun VacancyScreen(modifier: Modifier = Modifier) {
+ Box(modifier = modifier)
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/Const.kt b/app/src/main/java/ru/practicum/android/diploma/util/Const.kt
new file mode 100644
index 00000000000..ea797e5b37e
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/Const.kt
@@ -0,0 +1,3 @@
+package ru.practicum.android.diploma.util
+
+object Const
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/Debounce.kt b/app/src/main/java/ru/practicum/android/diploma/util/Debounce.kt
new file mode 100644
index 00000000000..dba7cb8c01d
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/Debounce.kt
@@ -0,0 +1,34 @@
+package ru.practicum.android.diploma.util
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.launch
+
+/** Задержка перед повторным поиском при вводе текста (ТЗ: 2000 мс). */
+const val SEARCH_DEBOUNCE_MS = 2_000L
+
+@OptIn(FlowPreview::class)
+fun Flow.debounceSearch(): Flow = debounce(SEARCH_DEBOUNCE_MS)
+
+/**
+ * Откладывает выполнение [action] на [delayMillis].
+ * При повторном вызове предыдущая отложенная операция отменяется.
+ */
+class Debounce(
+ private val scope: CoroutineScope,
+ private val delayMillis: Long = SEARCH_DEBOUNCE_MS,
+) {
+ private var job: Job? = null
+
+ operator fun invoke(action: suspend () -> Unit) {
+ job?.cancel()
+ job = scope.launch {
+ delay(delayMillis)
+ action()
+ }
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionChecker.kt b/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionChecker.kt
new file mode 100644
index 00000000000..13852bf3b1a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionChecker.kt
@@ -0,0 +1,8 @@
+package ru.practicum.android.diploma.util
+
+/**
+ * Проверка доступности сетевого подключения перед выполнением запросов к API.
+ */
+fun interface NetworkConnectionChecker {
+ fun isConnected(): Boolean
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionCheckerImpl.kt b/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionCheckerImpl.kt
new file mode 100644
index 00000000000..49682544701
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/NetworkConnectionCheckerImpl.kt
@@ -0,0 +1,26 @@
+package ru.practicum.android.diploma.util
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+
+class NetworkConnectionCheckerImpl(
+ context: Context,
+) : NetworkConnectionChecker {
+
+ private val connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ override fun isConnected(): Boolean {
+ val capabilities = connectivityManager.activeNetwork
+ ?.let { connectivityManager.getNetworkCapabilities(it) }
+ ?: return false
+
+ val hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ val hasTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
+
+ return hasInternet && hasTransport
+ }
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/extentions/IntExtentions.kt b/app/src/main/java/ru/practicum/android/diploma/util/extentions/IntExtentions.kt
new file mode 100644
index 00000000000..190f0fb29a0
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/extentions/IntExtentions.kt
@@ -0,0 +1,5 @@
+package ru.practicum.android.diploma.util.extentions
+
+fun Int.formatWithSpaces(): String {
+ return "%,d".format(this).replace(",", " ")
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/extentions/SalaryExtentions.kt b/app/src/main/java/ru/practicum/android/diploma/util/extentions/SalaryExtentions.kt
new file mode 100644
index 00000000000..eb9902e9f31
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/extentions/SalaryExtentions.kt
@@ -0,0 +1,25 @@
+package ru.practicum.android.diploma.util.extentions
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import ru.practicum.android.diploma.R
+import ru.practicum.android.diploma.domain.models.Salary
+
+@Composable
+fun Salary?.formatSalary(): String {
+ if (this == null) return stringResource(R.string.no_salary)
+
+ return buildString {
+ from?.let {
+ append("${stringResource(R.string.from)} ${it.formatWithSpaces()} ")
+ }
+ to?.let {
+ append(
+ "${
+ stringResource(R.string.to)
+ } ${it.formatWithSpaces()} "
+ )
+ }
+ append(currency)
+ }.trim()
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/extentions/VacancyExtentions.kt b/app/src/main/java/ru/practicum/android/diploma/util/extentions/VacancyExtentions.kt
new file mode 100644
index 00000000000..2997de0528a
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/extentions/VacancyExtentions.kt
@@ -0,0 +1,14 @@
+package ru.practicum.android.diploma.util.extentions
+
+import ru.practicum.android.diploma.domain.models.Vacancy
+
+fun Vacancy.formatDescription(): String {
+ return buildString {
+ append(name)
+ city?.let {
+ append(
+ ", $city"
+ )
+ }
+ }.trim()
+}
diff --git a/app/src/main/java/ru/practicum/android/diploma/util/extentions/ViewExtentions.kt b/app/src/main/java/ru/practicum/android/diploma/util/extentions/ViewExtentions.kt
new file mode 100644
index 00000000000..4d6f309ed9b
--- /dev/null
+++ b/app/src/main/java/ru/practicum/android/diploma/util/extentions/ViewExtentions.kt
@@ -0,0 +1,22 @@
+package ru.practicum.android.diploma.util.extentions
+
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentContainerView
+import com.google.android.material.bottomnavigation.BottomNavigationView
+
+fun FragmentContainerView.applySystemBarsPadding(bottomNavigationView: BottomNavigationView) {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+
+ view.setPadding(
+ 0,
+ systemBars.top,
+ 0,
+ if (!bottomNavigationView.isVisible) systemBars.bottom else 0
+ )
+
+ insets
+ }
+}
diff --git a/app/src/main/res/color/bottom_nav_colors.xml b/app/src/main/res/color/bottom_nav_colors.xml
new file mode 100644
index 00000000000..baf227b3931
--- /dev/null
+++ b/app/src/main/res/color/bottom_nav_colors.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 00000000000..63429340a9e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_cross.xml b/app/src/main/res/drawable/ic_cross.xml
new file mode 100644
index 00000000000..fa811ebf41b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cross.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorites_24.xml b/app/src/main/res/drawable/ic_favorites_24.xml
new file mode 100644
index 00000000000..248a15a0412
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorites_24.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml
new file mode 100644
index 00000000000..3d49a8c9154
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9cbf1..db23c9a0df2 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -2,169 +2,15 @@
+ android:viewportWidth="512"
+ android:viewportHeight="512">
+ android:fillColor="#1A1B22"
+ android:pathData="M0,0h512v512H0z" />
+ android:fillColor="#3772E7"
+ android:pathData="M0,0h256v256H0z" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:fillColor="#3772E7"
+ android:pathData="M256,256h256v256H256z" />
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 2b068d11462..1e9c6352767 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,30 +1,11 @@
+
-
-
-
-
-
-
-
-
+ android:viewportWidth="512"
+ android:viewportHeight="512">
-
\ No newline at end of file
+ android:fillColor="#FDFDFD"
+ android:fillType="evenOdd"
+ android:pathData="M191.893,151L191.893,179.966L321.107,179.966L321.107,151L191.893,151ZM156,194.448L184.714,194.448L184.714,361L156,361L156,194.448ZM328.286,194.448L357,194.448L357,360.346L328.286,360.346L328.286,194.448Z" />
+
diff --git a/app/src/main/res/drawable/ic_like_empty.xml b/app/src/main/res/drawable/ic_like_empty.xml
new file mode 100644
index 00000000000..74d67511229
--- /dev/null
+++ b/app/src/main/res/drawable/ic_like_empty.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_48.xml b/app/src/main/res/drawable/ic_logo_48.xml
new file mode 100644
index 00000000000..07228304ba3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_48.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_main_24.xml b/app/src/main/res/drawable/ic_main_24.xml
new file mode 100644
index 00000000000..3e08b646129
--- /dev/null
+++ b/app/src/main/res/drawable/ic_main_24.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_role_boatswain.xml b/app/src/main/res/drawable/ic_role_boatswain.xml
new file mode 100644
index 00000000000..756d7c85771
--- /dev/null
+++ b/app/src/main/res/drawable/ic_role_boatswain.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_role_captain.xml b/app/src/main/res/drawable/ic_role_captain.xml
new file mode 100644
index 00000000000..22f5758a8da
--- /dev/null
+++ b/app/src/main/res/drawable/ic_role_captain.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_role_cook.xml b/app/src/main/res/drawable/ic_role_cook.xml
new file mode 100644
index 00000000000..b42da81c256
--- /dev/null
+++ b/app/src/main/res/drawable/ic_role_cook.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_role_mate.xml b/app/src/main/res/drawable/ic_role_mate.xml
new file mode 100644
index 00000000000..f3becb99a06
--- /dev/null
+++ b/app/src/main/res/drawable/ic_role_mate.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 00000000000..77a71cbbeb7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 00000000000..660fff81fa8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_team_24.xml b/app/src/main/res/drawable/ic_team_24.xml
new file mode 100644
index 00000000000..7463bd93848
--- /dev/null
+++ b/app/src/main/res/drawable/ic_team_24.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_team_heart.xml b/app/src/main/res/drawable/ic_team_heart.xml
new file mode 100644
index 00000000000..9f3cbc79d38
--- /dev/null
+++ b/app/src/main/res/drawable/ic_team_heart.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_team_member_placeholder.xml b/app/src/main/res/drawable/ic_team_member_placeholder.xml
new file mode 100644
index 00000000000..70bfa8c59ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_team_member_placeholder.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/img_no_internet.png b/app/src/main/res/drawable/img_no_internet.png
new file mode 100644
index 00000000000..e9dbab638c1
Binary files /dev/null and b/app/src/main/res/drawable/img_no_internet.png differ
diff --git a/app/src/main/res/drawable/img_nothing_found.png b/app/src/main/res/drawable/img_nothing_found.png
new file mode 100644
index 00000000000..cd911e0971e
Binary files /dev/null and b/app/src/main/res/drawable/img_nothing_found.png differ
diff --git a/app/src/main/res/drawable/img_search_initial_placeholder.png b/app/src/main/res/drawable/img_search_initial_placeholder.png
new file mode 100644
index 00000000000..1b111303ac2
Binary files /dev/null and b/app/src/main/res/drawable/img_search_initial_placeholder.png differ
diff --git a/app/src/main/res/drawable/team_avatar_denis.png b/app/src/main/res/drawable/team_avatar_denis.png
new file mode 100644
index 00000000000..ecc3a45e140
Binary files /dev/null and b/app/src/main/res/drawable/team_avatar_denis.png differ
diff --git a/app/src/main/res/drawable/team_avatar_inna.png b/app/src/main/res/drawable/team_avatar_inna.png
new file mode 100644
index 00000000000..29badcb8e3f
Binary files /dev/null and b/app/src/main/res/drawable/team_avatar_inna.png differ
diff --git a/app/src/main/res/drawable/team_avatar_maria.png b/app/src/main/res/drawable/team_avatar_maria.png
new file mode 100644
index 00000000000..6517ae0bc22
Binary files /dev/null and b/app/src/main/res/drawable/team_avatar_maria.png differ
diff --git a/app/src/main/res/drawable/team_avatar_timofey.png b/app/src/main/res/drawable/team_avatar_timofey.png
new file mode 100644
index 00000000000..9f922718c92
Binary files /dev/null and b/app/src/main/res/drawable/team_avatar_timofey.png differ
diff --git a/app/src/main/res/font/ys_display_bold.ttf b/app/src/main/res/font/ys_display_bold.ttf
new file mode 100644
index 00000000000..f9b3f03cce5
Binary files /dev/null and b/app/src/main/res/font/ys_display_bold.ttf differ
diff --git a/app/src/main/res/font/ys_display_medium.ttf b/app/src/main/res/font/ys_display_medium.ttf
new file mode 100644
index 00000000000..cc63032e212
Binary files /dev/null and b/app/src/main/res/font/ys_display_medium.ttf differ
diff --git a/app/src/main/res/font/ys_display_regular.ttf b/app/src/main/res/font/ys_display_regular.ttf
new file mode 100644
index 00000000000..02173eb8290
Binary files /dev/null and b/app/src/main/res/font/ys_display_regular.ttf differ
diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml
index ea46b92d7b9..9a1f3aba5cd 100644
--- a/app/src/main/res/layout/activity_root.xml
+++ b/app/src/main/res/layout/activity_root.xml
@@ -1,19 +1,35 @@
-
-
+
-
\ No newline at end of file
+
+
diff --git a/app/src/main/res/menu/bottom_navigation_view.xml b/app/src/main/res/menu/bottom_navigation_view.xml
new file mode 100644
index 00000000000..341ba12faf2
--- /dev/null
+++ b/app/src/main/res/menu/bottom_navigation_view.xml
@@ -0,0 +1,16 @@
+
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index c209e78ecd3..9a2bb0406ce 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index b2dfe3d1ba5..9a2bb0406ce 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4f0f1d64e58..29f6db63ea7 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 62b611da081..29f6db63ea7 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 948a3070fe3..6d77347d595 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 1b9a6956b3a..6d77347d595 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 28d4b77f9f0..65f7f8a1e45 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 9287f508362..65f7f8a1e45 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index aa7d6427e6f..4f9d460ca2a 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 9126ae37cbc..4f9d460ca2a 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/navigation/navigation_graph.xml b/app/src/main/res/navigation/navigation_graph.xml
new file mode 100644
index 00000000000..398208dda8c
--- /dev/null
+++ b/app/src/main/res/navigation/navigation_graph.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index b5195a2f799..6eb43f3ee4a 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,7 +1,14 @@
-
-
+
+
-
\ No newline at end of file
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c8524cd961d..f0597f5db5f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,5 +1,7 @@
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
+ #1A1B22
+ #FDFDFD
+ #AEAFB4
+ #3772E7
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000000..045e125f3d8
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index db07d25c68b..fe198de0608 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,41 @@
Practicum-Android-Diploma
-
\ No newline at end of file
+ Поиск вакансий
+ Введите запрос
+ Главная
+ Избранное
+ Команда
+ Найдено
+ от
+ до
+ Зарплата не указана
+ Компания не указана
+
+
+ Нет интернета
+ Таких вакансий нет
+ Не удалось получить \n список вакансий
+
+
+
+ - Найдено %d вакансий
+ - Найдена %d вакансия
+ - Найдено %d вакансий
+ - Найдено %d вакансий
+ - Найдено %d вакансий
+
+
+ Над приложением\nработали
+ Бороздим океан вакансий для вас!
+
+
+ Инна Мелихова
+ Капитан
+ Мария Касаткина
+ Старпом
+ Денис Андрианов
+ Боцман
+ Тимофей Занин
+ Кок
+ Произошла ошибка
+
diff --git a/app/src/main/res/values/text_styles.xml b/app/src/main/res/values/text_styles.xml
new file mode 100644
index 00000000000..cdc5fbeb8d6
--- /dev/null
+++ b/app/src/main/res/values/text_styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 472697695a0..e1bf229ea7d 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,9 +1,14 @@
-
-
+
+
-
\ No newline at end of file
+
diff --git a/app/src/test/java/ru/practicum/android/diploma/util/DebounceTest.kt b/app/src/test/java/ru/practicum/android/diploma/util/DebounceTest.kt
new file mode 100644
index 00000000000..f6378a9f7dd
--- /dev/null
+++ b/app/src/test/java/ru/practicum/android/diploma/util/DebounceTest.kt
@@ -0,0 +1,23 @@
+package ru.practicum.android.diploma.util
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class DebounceTest {
+
+ @Test
+ fun `Debounce runs only last action after delay`() = runTest {
+ var count = 0
+ val debounce = Debounce(this)
+
+ debounce { count++ }
+ debounce { count++ }
+ advanceUntilIdle()
+
+ assertEquals(1, count)
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index bf0c8cbae51..f0a5d97e2e6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.android.application") version "8.12.0" apply false
- id("org.jetbrains.kotlin.android") version "2.3.10" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.ksp) apply false
+
id("convention.detekt")
}
diff --git a/develop.properties b/develop.properties
new file mode 100644
index 00000000000..a4634c24921
--- /dev/null
+++ b/develop.properties
@@ -0,0 +1 @@
+apiAccessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwcmFjdGljdW0ucnUiLCJhdWQiOiJwcmFjdGljdW0ucnUiLCJ1c2VybmFtZSI6InZhbHRoZXJ5cyJ9.vgJuWCdqyPJ20-P06CUAr0M8McLJhnIvFLzX4s7eGBw
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 59d04520c29..db0c604e097 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,29 @@
[versions]
# Build constants
+agp = "8.12.0"
+activityCompose = "1.10.0"
compileSdk = "36"
+fragmentKtx = "1.5.6"
java = "VERSION_17"
+lifecycleViewmodelCompose = "2.10.0"
material = "1.13.0"
+materialVersion = "1.8.0"
minSdk = "26"
+navigationFragmentKtx = "2.9.0"
targetSdk = "33"
+kotlin = "2.3.21"
+ksp = "2.3.7"
# AndroidX
appcompat = "1.7.1"
constraintlayout = "2.2.1"
coreKtx = "1.18.0"
+navigation="2.9.0"
+viewModel = "2.10.0"
+
+# Coroutines
+coroutines = "1.10.2"
# Testing
espressoCore = "3.5.1"
@@ -20,23 +33,68 @@ junitExt = "1.1.5"
# Static analysis
detekt = "1.23.8"
detektTwitterComposeRules = "0.0.26"
+legacySupportV4 = "1.0.0"
+lifecycleLivedataKtx = "2.10.0"
+lifecycleViewmodelKtx = "2.10.0"
+
+#Data
+retrofit = "3.0.0"
+room="2.7.1"
+
+#DI
+koin = "4.1.1"
+firebaseCrashlyticsBuildtools = "3.0.7"
+
+#Compose
+compose-bom = "2026.04.01"
+coil = "3.1.0"
[libraries]
# AndroidX
+coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
+navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
+activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
+coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
+coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
+androidx-viewmodel = {group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "viewModel" }
# UI
+fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
+lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
material = { module = "com.google.android.material:material", version.ref = "material" }
+#Data
+retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
+retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
+firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
+
+room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
+room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+
+#DI
+koin = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
+
+#Compose
+compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
+compose-ui = { module = "androidx.compose.ui:ui" }
+compose-coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
+
# Testing
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
junit4 = { module = "junit:junit", version.ref = "junit4" }
junit-ext = { module = "androidx.test.ext:junit", version.ref = "junitExt" }
# Static analysis
+material-v180 = { module = "com.google.android.material:material", version.ref = "materialVersion" }
+navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
+navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationFragmentKtx" }
+material3 = { module = "androidx.compose.material3:material3" }
staticAnalysis-detektApi = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
staticAnalysis-detektCli = { module = "io.gitlab.arturbosch.detekt:detekt-cli", version.ref = "detekt" }
staticAnalysis-detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
@@ -45,4 +103,17 @@ staticAnalysis-detektPlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gra
staticAnalysis-detektTest = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
staticAnalysis-detektTwitterComposeRules = { module = "com.twitter.compose.rules:detekt", version.ref = "detektTwitterComposeRules" }
+legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
+lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+ui = { module = "androidx.compose.ui:ui" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+
[bundles]