diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..f04d50d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,10 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +# Keep annotation definitions +-keep class org.koin.core.annotation.** { *; } + +# Keep classes annotated with Koin annotations +-keep @org.koin.core.annotation.* class * { *; } \ No newline at end of file diff --git a/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json index 4877ef0..853ed4f 100644 --- a/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json +++ b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "bf664fe902e116c42af432814d63d6a7", + "identityHash": "3e896e9a3d3b2f61149f8c0fde7e5964", "entities": [ { "tableName": "users", @@ -52,11 +52,69 @@ "id" ] } + }, + { + "tableName": "resumes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `experience_type` TEXT NOT NULL, `about_me` TEXT NOT NULL, `key_skills` TEXT NOT NULL, `position` TEXT NOT NULL, `from_salary` INTEGER, `to_salary` INTEGER, `recommended_skills` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experienceType", + "columnName": "experience_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "aboutMe", + "columnName": "about_me", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySkills", + "columnName": "key_skills", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromSalary", + "columnName": "from_salary", + "affinity": "INTEGER" + }, + { + "fieldPath": "toSalary", + "columnName": "to_salary", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendedSkills", + "columnName": "recommended_skills", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf664fe902e116c42af432814d63d6a7')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e896e9a3d3b2f61149f8c0fde7e5964')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/common/App.kt b/app/src/main/java/com/prodhack/moscow2025/common/App.kt index 00ce3ad..ce5a236 100644 --- a/app/src/main/java/com/prodhack/moscow2025/common/App.kt +++ b/app/src/main/java/com/prodhack/moscow2025/common/App.kt @@ -28,9 +28,7 @@ class App : Application() { androidContext(this@App) analytics() modules( - listOf( - AppModules().module - ) + AppModules().module ) } FirebaseApp.initializeApp(this@App) diff --git a/app/src/main/java/com/prodhack/moscow2025/common/di/AppModules.kt b/app/src/main/java/com/prodhack/moscow2025/common/di/AppModules.kt index 300fdfe..61c0d27 100644 --- a/app/src/main/java/com/prodhack/moscow2025/common/di/AppModules.kt +++ b/app/src/main/java/com/prodhack/moscow2025/common/di/AppModules.kt @@ -1,6 +1,7 @@ package com.prodhack.moscow2025.common.di import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module /** diff --git a/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt index 2c55559..2ab5866 100644 --- a/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt +++ b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt @@ -1,7 +1,11 @@ package com.prodhack.moscow2025.common.di +import android.content.Context +import androidx.room.Room +import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module +import org.koin.core.annotation.Single @Module @ComponentScan("com.prodhack.moscow2025.presentation") @@ -13,4 +17,13 @@ class DomainModule @Module @ComponentScan("com.prodhack.moscow2025.data") -class DataModule +class DataModule{ + @Single + fun provideDatabase(context: Context): AppDatabase = + Room.databaseBuilder( + context, + AppDatabase::class.java, + "t_tasks.db" + ).fallbackToDestructiveMigration() + .build() +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt deleted file mode 100644 index 17dcbd9..0000000 --- a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.prodhack.moscow2025.data.base - - -interface BaseEntity { - val id: Number -} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt index ddf62da..3d0b4e5 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt @@ -8,7 +8,7 @@ import androidx.room.RoomDatabase import androidx.room.withTransaction @OptIn(ExperimentalPagingApi::class) -class BaseRemoteMediator( +class BaseRemoteMediator( private val db: RoomDatabase, private val dao: BasePaginationDAO, private val makeRequest: suspend (page: Long, pageCount: Int) -> Result> @@ -26,17 +26,12 @@ class BaseRemoteMediator( ) LoadType.APPEND -> { - val lastItem = state.lastItemOrNull() - if (lastItem == null) { - 1 - } else { - (lastItem.id.toLong() / state.config.pageSize) + 1 - } + state.pages.size + 1 } } val result = makeRequest( - loadKey, + (loadKey.toLong() - 1) * state.config.pageSize, state.config.pageSize ) @@ -46,8 +41,7 @@ class BaseRemoteMediator( if (loadType == LoadType.REFRESH) { dao.clearAll() } - val beerEntities = data - dao.upsertAll(beerEntities) + dao.upsertAll(data) } MediatorResult.Success( endOfPaginationReached = data.size < state.config.pageSize diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt index 21f0771..a875241 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt @@ -19,157 +19,157 @@ import kotlin.time.Duration abstract class BaseRepository { - // Caching module ============================================================================== - private val internalCacheStorage = mutableMapOf>() + // Caching module ============================================================================== + private val internalCacheStorage = mutableMapOf>() - private data class CacheEntry( - val value: T, - val expirationTime: Long - ) + private data class CacheEntry( + val value: T, + val expirationTime: Long + ) - fun putCache(cacheConfiguration: Pair, value: T) { - internalCacheStorage[cacheConfiguration.first] = - CacheEntry(value, cacheConfiguration.second.inWholeSeconds) - } + fun putCache(cacheConfiguration: Pair, value: T) { + internalCacheStorage[cacheConfiguration.first] = + CacheEntry(value, cacheConfiguration.second.inWholeSeconds) + } - @Suppress("UNCHECKED_CAST") - fun getCache(key: String): T? { - val entry = internalCacheStorage[key] ?: return null - if (entry.expirationTime < System.currentTimeMillis()) { - internalCacheStorage.remove(key) - return null - } - return entry.value as T - } + @Suppress("UNCHECKED_CAST") + fun getCache(key: String): T? { + val entry = internalCacheStorage[key] ?: return null + if (entry.expirationTime < System.currentTimeMillis()) { + internalCacheStorage.remove(key) + return null + } + return entry.value as T + } - // Base data sources =========================================================================== + // Base data sources =========================================================================== - protected open val defaultKtorClient: HttpClient? = null - protected open val db: RoomDatabase? = null + protected open val defaultKtorClient: HttpClient? = null + protected open val db: RoomDatabase? = null - companion object { - private const val TAG = "BaseRepository" - } + companion object { + private const val TAG = "BaseRepository" + } - // Internal methods ============================================================================ + // Internal methods ============================================================================ - private fun assertKtorClientSpecify() { - if (defaultKtorClient == null) { - Log.e(TAG, "You must specify ktor client for make network requests") - throw IllegalStateException("You must specify ktor client for make network requests") - } - } + private fun assertKtorClientSpecify() { + if (defaultKtorClient == null) { + Log.e(TAG, "You must specify ktor client for make network requests") + throw IllegalStateException("You must specify ktor client for make network requests") + } + } - private fun assertDBSpecify() { - if (db == null) { - throw IllegalStateException("You must specify db for use pagination/cashing") - } - } + private fun assertDBSpecify() { + if (db == null) { + throw IllegalStateException("You must specify db for use pagination/cashing") + } + } - // And methods for use :) ====================================================================== + // And methods for use :) ====================================================================== - /** - * Makes a network request using the provided Ktor client and request builder block. - * - * This function handles the common boilerplate for making a network request, - * including error handling and converting exceptions to a domain-specific `NetworkError`. - * - * @param T The expected successful response type. This type must be deserializable by Ktor. - * @param ktorClient The [HttpClient] to use for the request. Defaults to `this.defaultKtorClient`. - * An [IllegalStateException] will be thrown if no client is provided and `defaultKtorClient` is null. - * @param block A lambda function that configures the [HttpRequestBuilder] for the request. - * @return A [Result] object containing either the successful response of type [T] or a [NetworkError] if the request fails. - * @throws IllegalStateException if `ktorClient` is null and `defaultKtorClient` is also null. - */ - internal suspend inline fun networkRequest( - ktorClient: HttpClient? = this.defaultKtorClient, - cacheConfiguration: Pair? = null, - block: HttpRequestBuilder.() -> Unit - ): Result { - Log.d(TAG, "Network request! Asserting ktor client specify") - assertKtorClientSpecify() - Log.d(TAG, "ktor client is specified - continue network request") - return try { - Log.d(TAG, "Start request!") - val response = ktorClient!!.request(block = block) - Log.d(TAG, "Request was made without exceptions") + /** + * Makes a network request using the provided Ktor client and request builder block. + * + * This function handles the common boilerplate for making a network request, + * including error handling and converting exceptions to a domain-specific `NetworkError`. + * + * @param T The expected successful response type. This type must be deserializable by Ktor. + * @param ktorClient The [HttpClient] to use for the request. Defaults to `this.defaultKtorClient`. + * An [IllegalStateException] will be thrown if no client is provided and `defaultKtorClient` is null. + * @param block A lambda function that configures the [HttpRequestBuilder] for the request. + * @return A [Result] object containing either the successful response of type [T] or a [NetworkError] if the request fails. + * @throws IllegalStateException if `ktorClient` is null and `defaultKtorClient` is also null. + */ + internal suspend inline fun networkRequest( + ktorClient: HttpClient? = this.defaultKtorClient, + cacheConfiguration: Pair? = null, + block: HttpRequestBuilder.() -> Unit + ): Result { + Log.d(TAG, "Network request! Asserting ktor client specify") + assertKtorClientSpecify() + Log.d(TAG, "ktor client is specified - continue network request") + return try { + Log.d(TAG, "Start request!") + val response = ktorClient!!.request(block = block) + Log.d(TAG, "Request was made without exceptions") - if (response.status.isSuccess()) { - Result.success( - value = response - ).map { - it.body() - } - } else { - val firstCodeNum = response.status.value / 100 - val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown" - Result.failure( - when (firstCodeNum) { - 4 -> NetworkError.InputError(detail) - else -> NetworkError.Unexpected(detail) - } - ) - } - } catch (e: Exception) { - Log.e(TAG, "Exception in request process! $e") - Result.failure( - exception = e.convertToNetworkError() - ) - }.onSuccess { - Log.v(TAG, "Network request was successful") - if (cacheConfiguration != null) { - putCache(cacheConfiguration, it) - } - }.onFailure { - Log.e(TAG, "Network request has error! $it") - } - } + if (response.status.isSuccess()) { + Result.success( + value = response + ).map { + it.body() + } + } else { + val firstCodeNum = response.status.value / 100 + val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown" + Result.failure( + when (firstCodeNum) { + 4 -> NetworkError.InputError(detail) + else -> NetworkError.Unexpected(detail) + } + ) + } + } catch (e: Exception) { + Log.e(TAG, "Exception in request process! $e") + Result.failure( + exception = e.convertToNetworkError() + ) + }.onSuccess { + Log.v(TAG, "Network request was successful") + if (cacheConfiguration != null) { + putCache(cacheConfiguration, it) + } + }.onFailure { + Log.e(TAG, "Network request has error! $it") + } + } - internal suspend inline fun internalCachedRequest( - ktorClient: HttpClient? = this.defaultKtorClient, - cacheConfiguration: Pair, - block: HttpRequestBuilder.() -> Unit - ): Result { - val cachedResult = getCache(cacheConfiguration.first) + internal suspend inline fun internalCachedRequest( + ktorClient: HttpClient? = this.defaultKtorClient, + cacheConfiguration: Pair, + block: HttpRequestBuilder.() -> Unit + ): Result { + val cachedResult = getCache(cacheConfiguration.first) - return if (cachedResult != null) { - Result.success(cachedResult) - } else { - networkRequest(ktorClient, cacheConfiguration, block) - } - } + return if (cachedResult != null) { + Result.success(cachedResult) + } else { + networkRequest(ktorClient, cacheConfiguration, block) + } + } - @OptIn(ExperimentalPagingApi::class) - protected fun paginatedRequest( - pageSize: Int = 10, - prefetchDistance: Int = pageSize, - enablePlaceholders: Boolean = true, - initialLoadSize: Int = pageSize * 3, - maxSize: Int = Int.MAX_VALUE, - jumpThreshold: Int = Int.MIN_VALUE, - dbDao: BasePaginationDAO, - makeRequest: suspend (page: Long, pageSize: Int) -> Result> - ): Flow> { - assertDBSpecify() + @OptIn(ExperimentalPagingApi::class) + protected fun paginatedRequest( + pageSize: Int = 10, + prefetchDistance: Int = pageSize, + enablePlaceholders: Boolean = true, + initialLoadSize: Int = pageSize * 3, + maxSize: Int = Int.MAX_VALUE, + jumpThreshold: Int = Int.MIN_VALUE, + dbDao: BasePaginationDAO, + makeRequest: suspend (offset: Long, pageSize: Int) -> Result> + ): Flow> { + assertDBSpecify() - return Pager( - config = PagingConfig( - pageSize, - prefetchDistance, - enablePlaceholders, - initialLoadSize, - maxSize, - jumpThreshold - ), - remoteMediator = BaseRemoteMediator( - db = db!!, - dao = dbDao, - makeRequest = makeRequest - ), - pagingSourceFactory = { - dbDao.getPaginatedData() - } - ).flow - } + return Pager( + config = PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholders, + initialLoadSize, + maxSize, + jumpThreshold + ), + remoteMediator = BaseRemoteMediator( + db = db!!, + dao = dbDao, + makeRequest = makeRequest + ), + pagingSourceFactory = { + dbDao.getPaginatedData() + } + ).flow + } } diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt deleted file mode 100644 index 60c8262..0000000 --- a/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.prodhack.moscow2025.data.base - -interface DBMappableDTO { - fun mapToDB(): T -} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt deleted file mode 100644 index 204b7d3..0000000 --- a/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.prodhack.moscow2025.data.base - -interface DomainMappableDTO { - fun mapToDomain(): T -} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt index db2fb77..76fd244 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt @@ -3,11 +3,13 @@ package com.prodhack.moscow2025.data.data_providers.local_db import androidx.room.Database import androidx.room.RoomDatabase import com.prodhack.moscow2025.data.data_providers.local_db.dao.CleanUpDao +import com.prodhack.moscow2025.data.data_providers.local_db.dao.ResumeDao import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao +import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity @Database( - entities = [UserEntity::class], + entities = [UserEntity::class, ResumeEntity::class], version = 1, exportSchema = true ) @@ -15,4 +17,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun cleanUpDao(): CleanUpDao + abstract fun resumeDao(): ResumeDao } diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt index 6b1976b..be57512 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt @@ -8,12 +8,5 @@ import org.koin.core.annotation.Single @Module class DatabaseProvider { - @Single - fun provideDatabase(context: Context): AppDatabase = - Room.databaseBuilder( - context, - AppDatabase::class.java, - "t_tasks.db" - ).fallbackToDestructiveMigration() - .build() + } diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/ResumeDao.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/ResumeDao.kt new file mode 100644 index 0000000..4e8fd60 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/ResumeDao.kt @@ -0,0 +1,21 @@ +package com.prodhack.moscow2025.data.data_providers.local_db.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import com.prodhack.moscow2025.data.base.BasePaginationDAO +import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity + +@Dao +interface ResumeDao: BasePaginationDAO { + + @Query("DELETE FROM resumes") + override suspend fun clearAll() + + @Upsert + override suspend fun upsertAll(data: List) + + @Query("SELECT * FROM resumes") + override fun getPaginatedData(): PagingSource +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/ResumeEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/ResumeEntity.kt new file mode 100644 index 0000000..fc73756 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/ResumeEntity.kt @@ -0,0 +1,37 @@ +package com.prodhack.moscow2025.data.data_providers.local_db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.prodhack.moscow2025.domain.models.ExperienceType +import com.prodhack.moscow2025.domain.models.ResumeModel +import kotlin.math.exp + +@Entity(tableName = "resumes") +data class ResumeEntity( + @PrimaryKey(autoGenerate = false) + val id: String, + @ColumnInfo("experience_type") + val experienceType: String, + @ColumnInfo("about_me") + val aboutMe: String, + @ColumnInfo("key_skills") + val keySkills: String, + val position: String, + @ColumnInfo("from_salary") + val fromSalary: Int?, + @ColumnInfo("to_salary") + val toSalary: Int?, + @ColumnInfo("recommended_skills") + val recommendedSkills: String +) { + fun mapToDomain(): ResumeModel = ResumeModel( + id = id, + position = position, + about = aboutMe, + experienceType = ExperienceType.valueOf(experienceType), + skills = keySkills.split("|"), + prediction = Pair(fromSalary, toSalary), + recommendedSkills = recommendedSkills.split("|") + ) +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt index 1870dce..60b6f0b 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt @@ -7,33 +7,6 @@ import com.prodhack.moscow2025.domain.models.User import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -@Serializable -data class ErrorNetworkDTO( - val detail: String -) - -@Serializable -data class UserPatchRequest( - val email: String?, - @SerialName("display_name") - val displayName: String? = null, - @SerialName("first_name") - val firstName: String? = null, - @SerialName("last_name") - val lastName: String? = null, - @SerialName("avatar_url") - val avatarUrl: String? = null, - val phone: String? = null, -) - -fun UpdateUserData.mapToData(): UserPatchRequest = UserPatchRequest( - email = email, - displayName = displayName, - firstName = firstName, - lastName = lastName, - avatarUrl = avatarUrl, - phone = phone -) @Serializable data class UserLoginRequest( @@ -57,29 +30,4 @@ fun RegisterData.mapToData(): UserRegisterRequest = UserRegisterRequest(email, p data class TokenResponse( @SerialName("access_token") val token: String -) - -@Serializable -data class UserResponse( - val id: String, - val email: String, - @SerialName("display_name") - val displayName: String? = null, - @SerialName("first_name") - val firstName: String? = null, - @SerialName("last_name") - val lastName: String? = null, - @SerialName("avatar_url") - val avatarUrl: String? = null, - val phone: String? = null, -) { - fun mapToDomain(): User = User( - id = id, - email = email, - displayName = displayName, - firstName = firstName, - lastName = lastName, - avatarUrl = avatarUrl, - phone = phone - ) -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/ErrorNetworkDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/ErrorNetworkDTO.kt new file mode 100644 index 0000000..ab505f8 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/ErrorNetworkDTO.kt @@ -0,0 +1,8 @@ +package com.prodhack.moscow2025.data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorNetworkDTO( + val detail: String +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt new file mode 100644 index 0000000..15f70e8 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt @@ -0,0 +1,85 @@ +package com.prodhack.moscow2025.data.dto + +import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity +import com.prodhack.moscow2025.domain.models.ExperienceType +import com.prodhack.moscow2025.domain.models.ResumeModel +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ExperienceTypeDTO { + @SerialName("noExperience") + NoExperience, + + @SerialName("lessThan1") + LessThan1, + + @SerialName("between1And3") + Between1And3, + + @SerialName("between3And6") + Between3And6, + + @SerialName("moreThan6") + MoreThan6; + + fun mapToDomain(): ExperienceType = when (this) { + NoExperience -> ExperienceType.NoExperience + LessThan1 -> ExperienceType.LessThan1 + Between1And3 -> ExperienceType.Between1And3 + Between3And6 -> ExperienceType.Between3And6 + MoreThan6 -> ExperienceType.MoreThan6 + } +} + +@Serializable +data class ResumeDTO( + val id: String, + @SerialName("experience_type") + val experienceType: ExperienceTypeDTO, + @SerialName("about_me") + val aboutMe: String, + @SerialName("key_skills") + val keySkills: List, + val position: String, + val prediction: PredictionDTO +) { + fun mapToDomain(): ResumeModel = ResumeModel( + id = id, + about = aboutMe, + skills = keySkills, + position = position, + experienceType = experienceType.mapToDomain(), + prediction = Pair( + prediction.fromSalary.toIntOrNull(), + prediction.toSalary.toIntOrNull() + ), + recommendedSkills = prediction.recommendedSkills + ) + + fun mapToDB(): ResumeEntity = ResumeEntity( + id = id, + aboutMe = aboutMe, + keySkills = keySkills.joinToString("|"), + position = position, + fromSalary = prediction.fromSalary.toIntOrNull(), + toSalary = prediction.toSalary.toIntOrNull(), + recommendedSkills = prediction.recommendedSkills.joinToString("|"), + experienceType = experienceType.mapToDomain().name + ) +} + +@Serializable +data class PredictionDTO( + @SerialName("from_salary") + val fromSalary: String, + @SerialName("to_salary") + val toSalary: String, + @SerialName("recommended_skills") + val recommendedSkills: List +) + +@Serializable +data class ResumeListDTO( + val resumes: List +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/UsersDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/UsersDtos.kt new file mode 100644 index 0000000..bed0054 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/UsersDtos.kt @@ -0,0 +1,54 @@ +package com.prodhack.moscow2025.data.dto + +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.models.User +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserPatchRequest( + val email: String?, + @SerialName("display_name") + val displayName: String? = null, + @SerialName("first_name") + val firstName: String? = null, + @SerialName("last_name") + val lastName: String? = null, + @SerialName("avatar_url") + val avatarUrl: String? = null, + val phone: String? = null, +) + +fun UpdateUserData.mapToData(): UserPatchRequest = UserPatchRequest( + email = email, + displayName = displayName, + firstName = firstName, + lastName = lastName, + avatarUrl = avatarUrl, + phone = phone +) + +@Serializable +data class UserResponse( + val id: String, + val email: String, + @SerialName("display_name") + val displayName: String? = null, + @SerialName("first_name") + val firstName: String? = null, + @SerialName("last_name") + val lastName: String? = null, + @SerialName("avatar_url") + val avatarUrl: String? = null, + val phone: String? = null, +) { + fun mapToDomain(): User = User( + id = id, + email = email, + displayName = displayName, + firstName = firstName, + lastName = lastName, + avatarUrl = avatarUrl, + phone = phone + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt new file mode 100644 index 0000000..553ca44 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt @@ -0,0 +1,40 @@ +package com.prodhack.moscow2025.data.repImplementations + +import androidx.paging.map +import com.prodhack.moscow2025.data.base.BaseRepository +import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient +import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase +import com.prodhack.moscow2025.data.dto.ResumeListDTO +import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import com.prodhack.moscow2025.domain.models.ResumeModel +import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper +import io.ktor.client.request.url +import io.ktor.http.HttpMethod +import kotlinx.coroutines.flow.map +import org.koin.core.annotation.Single + +@Single +class ResumeRepositoryImpl( + ktorClient: ApiKtorClient, + override val db: AppDatabase +) : ResumeRepository, BaseRepository() { + + override val defaultKtorClient = ktorClient.client + + private val resumeDao = db.resumeDao() + + override fun loadResumeList(): RemotePagingWrapper = paginatedRequest( + pageSize = 20, + dbDao = resumeDao, + makeRequest = { offset, pageSize -> + networkRequest { + method = HttpMethod.Get + url { + url("/resume/list") + parameters.append("limit", pageSize.toString()) + parameters.append("offset", offset.toString()) + } + }.map { it -> it.resumes.map { it.mapToDB() } } + } + ).map { it -> it.map { it.mapToDomain() } } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt new file mode 100644 index 0000000..6fbe291 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt @@ -0,0 +1,8 @@ +package com.prodhack.moscow2025.domain.interfaces.resumes + +import com.prodhack.moscow2025.domain.models.ResumeModel +import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper + +interface ResumeRepository { + fun loadResumeList(): RemotePagingWrapper +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt new file mode 100644 index 0000000..967beeb --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt @@ -0,0 +1,19 @@ +package com.prodhack.moscow2025.domain.models + +data class ResumeModel( + val id: String, + val position: String, + val about: String, + val skills: List, + val experienceType: ExperienceType, + val prediction: Pair, + val recommendedSkills: List +) + +enum class ExperienceType { + NoExperience, + LessThan1, + Between1And3, + Between3And6, + MoreThan6 +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt index c7e073e..b8c6928 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt @@ -1,5 +1,6 @@ package com.prodhack.moscow2025.domain.usecase.auth +import android.util.Log import com.prodhack.moscow2025.domain.interfaces.AuthRepository import com.prodhack.moscow2025.domain.interfaces.UserRepository import kotlinx.coroutines.flow.firstOrNull @@ -16,14 +17,22 @@ class CheckSessionUseCase( private val authRepository: AuthRepository, private val userRepository: UserRepository ) { + + private companion object { + const val TAG = "CheckSessionUseCase" + } + /** - * return session state with + * @return session state in enum format [SessionState] */ suspend operator fun invoke(): SessionState = if (authRepository.fetchLoginState().firstOrNull() == true) { + Log.d(TAG, "user authorized, requesting profile") if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) { + Log.d(TAG, "user authorized, first name is blank -> need fill profile") SessionState.NotFilledProfile } else { + Log.d(TAG, "user authorized, first name is filled -> user already fill profile") SessionState.FilledAndAuthorized } } else { diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt new file mode 100644 index 0000000..ab40b6d --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt @@ -0,0 +1,40 @@ +package com.prodhack.moscow2025.domain.usecase.resumes + +import androidx.paging.PagingData +import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import com.prodhack.moscow2025.domain.models.ExperienceType +import com.prodhack.moscow2025.domain.models.ResumeModel +import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper +import kotlinx.coroutines.flow.flow +import org.koin.core.annotation.Single + +@Single +class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) { +// operator fun invoke(): RemotePagingWrapper = resumeRepository.loadResumeList() + + // Mocked data + operator fun invoke(): RemotePagingWrapper = flow { + emit( + PagingData.from( + listOf( + ResumeModel( + id = "iajxioasdkmcaolsd,c", + position = "Android разработчик", + about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " + + "И нет это я не про себя, это просто какие-то данные," + + " чтобы проверить, что это чудовище работает", + skills = listOf( + "Android SDK", + "Kotlin", + "Room", + "Ktor" + ), + experienceType = ExperienceType.Between3And6, + prediction = Pair(200000, 230000), + recommendedSkills = listOf("KMP") + ) + ) + ) + ) + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt index be63580..2acf616 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt @@ -26,6 +26,10 @@ import kotlin.getValue class MainActivity : ComponentActivity() { + private companion object { + const val TAG = "MainActivity" + } + private val checkSessionUseCase: CheckSessionUseCase by inject() private val sessionDestinationState = MutableStateFlow(null) @@ -42,8 +46,11 @@ class MainActivity : ComponentActivity() { runBlocking { val sessionState = try { - checkSessionUseCase() + checkSessionUseCase().also { + Log.d(TAG, "SessionState received $it") + } } catch (e: Exception) { + Log.e(TAG, "Exception in session state getting process", e) SessionState.NotAuthorized } sessionDestinationState.value = @@ -67,7 +74,6 @@ class MainActivity : ComponentActivity() { .addOnCompleteListener { task -> if (task.isSuccessful) { val token = task.result - Log.d("TOKEN", token) } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt index d1c6b29..687246b 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,17 +29,17 @@ fun TopLogo( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - Image( + Icon( modifier = Modifier.size(100.dp), - painter = painterResource(R.drawable.ic_launcher_foreground), + painter = painterResource(R.drawable.app_logo), contentDescription = "App logo" ) Spacer(modifier = Modifier.width(Paddings.medium)) Text( text = stringResource(R.string.app_name), - style = MaterialTheme.typography.titleLarge, - fontSize = 48.sp + style = MaterialTheme.typography.titleMedium, + fontSize = 24.sp ) } } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt new file mode 100644 index 0000000..c7321f1 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt @@ -0,0 +1,17 @@ +package com.prodhack.moscow2025.presentation.dataModels + +import com.prodhack.moscow2025.domain.models.ResumeModel + +data class UIResumeBaseInfo( + val id: String, + val positionName: String, + val salary: String +) + +fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo( + id = id, + positionName = position, + salary = prediction.first?.let { from -> + prediction.second?.let { to -> "$from-$to" } ?: from.toString() + } ?: prediction.second?.toString() ?: "Ошибка" +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt index 924fe40..8e7629a 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt @@ -28,7 +28,7 @@ fun TTasksApp( context: Context, sessionDestination: AppDestination? = null ) { - MoscowHackatonTemplateTheme() { + MoscowHackatonTemplateTheme { val snackbarHostState = remember { SnackbarHostState() } val bottomBarState = remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt index 73385ab..d9eac33 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt @@ -144,11 +144,10 @@ fun ErrorCollectorScope.FillProfileScreen( style = typography.titleLarge, fontSize = 31.sp ) - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), + Icon( + painter = painterResource(R.drawable.app_logo), contentDescription = null, - modifier = Modifier.size(140.dp), - contentScale = ContentScale.Crop + modifier = Modifier.size(140.dp) ) } Spacer(Modifier.height(20.dp)) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt index 5acd578..0bcf9e2 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState @@ -127,8 +128,8 @@ fun ErrorCollectorScope.LoginScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), + Icon( + painter = painterResource(R.drawable.app_logo), contentDescription = null, modifier = Modifier .size(200.dp) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt index e7f84d3..b85ce7c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt @@ -1,9 +1,39 @@ package com.prodhack.moscow2025.presentation.screens.main +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.compose.collectAsLazyPagingItems +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.components.standart.BigButton +import com.prodhack.moscow2025.presentation.components.standart.TTFloatingActionButton +import com.prodhack.moscow2025.presentation.components.standart.TopLogo +import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo +import com.prodhack.moscow2025.presentation.theme.Paddings import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope import org.koin.androidx.compose.koinViewModel @@ -14,269 +44,138 @@ fun ErrorCollectorScope.MainScreen( modifier: Modifier = Modifier, viewModel: MainScreenViewModel = koinViewModel() ) { - Text("Main screen will be here soon") -// val openCalendarModal = remember { mutableStateOf(false) } -// val openTaskAddSheet = remember { mutableStateOf(false) } -// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) -// val tasks = viewModel.taskList.collectAsLazyPagingItems() -// -// val selectedTask = remember { mutableStateOf(null) } -// -// Box( -// modifier = modifier -// .fillMaxSize() -// .padding(horizontal = Paddings.large), -// contentAlignment = Alignment.BottomCenter -// ) { -// Column( -// modifier = Modifier.fillMaxSize(), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Spacer(modifier = Modifier.height(Paddings.large)) -// TopLogo() -// Spacer(modifier = Modifier.height(Paddings.large)) -// -// MainScreenFilters(viewModel = viewModel) { -// openCalendarModal.value = true -// } -// -// Spacer(modifier = Modifier.height(Paddings.large)) -// -// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics -> -// BubbledCategoryFilters( -// categories = topics, -// selectedItemId = viewModel.selectedTopicId.value ?: -1 -// ) { categoryId -> -// viewModel.selectTopic(categoryId) -// } -// } -// Spacer(modifier = Modifier.height(Paddings.large)) -// -// if (tasks.loadState.hasError) { -// Text( -// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!", -// style = Typography.titleMedium, -// textAlign = TextAlign.Center, -// fontSize = 18.sp, -// color = MaterialTheme.colorScheme.error -// ) -// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) { -// Spacer(modifier = Modifier.weight(1f)) -// -// Text( -// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку", -// style = Typography.titleMedium, -// textAlign = TextAlign.Center, -// fontSize = 18.sp, -// color = MaterialTheme.colorScheme.onBackground -// ) -// Spacer(modifier = Modifier.height(Paddings.large)) -// BigButton(buttonText = "Начать", onClick = { -// -// }, isLoading = false) -// -// Spacer(modifier = Modifier.weight(3f)) -// -// } else { -// LazyColumn( -// verticalArrangement = Arrangement.spacedBy(Paddings.small), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// items(tasks.itemCount) { it -> -// val task = tasks[it] -// task?.let { -// TaskCard( -// onClick = { -// selectedTask.value = it -// }, -// taskInfo = it, -// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value -// ) { -// viewModel.toggleTaskAsDone( -// tripId = it.tripId, -// taskId = it.id, -// currState = it.archived -// ) -// tasks.refresh() -// } -// } -// } -// -// item { -// if (!tasks.loadState.append.endOfPaginationReached) { -// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) -// } -// } -// } -// } -// } -// -// TTFloatingActionButton( -// modifier = Modifier -// .align(Alignment.BottomCenter) -// .padding(bottom = Paddings.medium), -// onClick = { -// openTaskAddSheet.value = true -// }, -// text = "Добавить задачу" -// ) -// } -// -// -// AnimatedVisibility(openCalendarModal.value) { -// DateRangePickerModal({ -// Log.d("DatePicker", it.toString()) -// if (it.first != null && it.second != null) { -// viewModel.setDate(Pair(it.first!!, it.second!!)) -// openCalendarModal.value = false -// } -// }) { -// openCalendarModal.value = false -// } -// } -// -// if (openTaskAddSheet.value) { -// AddTaskBottomSheet( -// sheetState = sheetState, -// onDismiss = { -// openTaskAddSheet.value = false -// } -// ) -// } -// -// val cs = MaterialTheme.colorScheme -// -// val viewSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) -// -// if (selectedTask.value != null) { -// -// val openCalendarModal2 = remember { mutableStateOf(false) } -// -// ModalBottomSheet( -// onDismissRequest = { -// selectedTask.value = null -// }, -// sheetState = viewSheetState, -// dragHandle = {}, -// shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) -// ) { -// Column( -// modifier = Modifier -// .padding(horizontal = 24.dp, vertical = 16.dp) -// .verticalScroll(rememberScrollState()), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Text( -// text = "Просмотр задачи", -// color = cs.onSurface, -// style = Typography.titleMedium, -// fontSize = 22.sp, -// textAlign = TextAlign.Center, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.medium)) -// -// Text( -// text = selectedTask.value!!.name, -// color = cs.onSurface, -// style = Typography.titleMedium, -// fontSize = 20.sp, -// textAlign = TextAlign.Center, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.medium)) -// -// -// Text( -// text = "Что нужно сделать", -// color = cs.onSurface, -// style = Typography.titleMedium, -// fontSize = 18.sp, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.small)) -// -// Text( -// text = selectedTask.value!!.whatNeedToDo, -// color = cs.onSurface, -// style = Typography.labelLarge, -// fontSize = 16.sp, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.medium)) -// -// Text( -// text = "Для чего", -// color = cs.onSurface, -// style = Typography.titleMedium, -// fontSize = 18.sp, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.small)) -// -// Text( -// text = selectedTask.value!!.reason, -// color = cs.onSurface, -// style = Typography.labelLarge, -// fontSize = 16.sp, -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 24.dp, top = 8.dp) -// ) -// -// Spacer(modifier = Modifier.height(Paddings.large)) -// -// TTTextField( -// onClick = { -// openCalendarModal2.value = true -// }, -// value = timestampToDateWithYear(selectedTask.value!!.deadline), -// readOnly = true, -// onValueChange = {}, -// label = "Дедлайн", -// trailingIcon = { -// Icon( -// modifier = Modifier -// .size(24.dp), -// painter = painterResource( -// R.drawable.ic_calendar -// ), -// tint = MaterialTheme.colorScheme.onPrimary, -// contentDescription = null -// ) -// } -// ) -// } -// } -// -// AnimatedVisibility(openCalendarModal2.value) { -// DatePickerModal({ -// Log.d("DatePicker", it.toString()) -// it?.let { date -> -// viewModel.changeTaskDeadline(selectedTask.value, date) -// selectedTask.value = null -// openCalendarModal.value = false -// } -// }) { -// openCalendarModal.value = false -// } -// } -// } + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + val shapes = MaterialTheme.shapes + + Box { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TopLogo() + Spacer(modifier = Modifier.height(Paddings.medium)) + Text( + text = "Ваши резюме", + style = typography.titleLarge, + fontSize = 32.sp, + color = colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(Paddings.large)) + + val items = viewModel.resumeList.collectAsLazyPagingItems() + + if (items.itemCount == 0 && items.loadState.append.endOfPaginationReached) { + Text( + text = "Здесь пока ничего нет", + style = typography.labelLarge, + textAlign = TextAlign.Center, + fontSize = 24.sp, + color = colorScheme.onBackground + ) + + BigButton(onClick = { + TODO() + }, buttonText = "Создать резюме", isLoading = false) + } else if (items.loadState.hasError) { + Text( + modifier = Modifier + .fillMaxWidth() + .background(colorScheme.error, shape = shapes.small) + .padding(Paddings.medium), + text = "Кажется что-то пошло не так, но мы уже чиним 🛠️", + style = typography.labelLarge, + textAlign = TextAlign.Center, + fontSize = 24.sp, + color = colorScheme.onError + ) + } else { + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + Paddings.medium + ) + ) { + items(items.itemCount) { + val resume = items[it] + resume?.let { + ResumeShortInfoCard(info = it) { + + } + } + } + + item { + if (items.loadState.append.endOfPaginationReached.not()) { + CircularProgressIndicator() + } + } + } + } + } + + val context = LocalContext.current + TTFloatingActionButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = Paddings.medium), + onClick = { + Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show() + }, + text = "Добавить резюме" + ) + } +} + +@Composable +fun ResumeShortInfoCard( + modifier: Modifier = Modifier, + info: UIResumeBaseInfo, + onClick: () -> Unit +) { + val typography = MaterialTheme.typography + Card( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + onClick = onClick + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(Paddings.medium), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + verticalArrangement = Arrangement.spacedBy(Paddings.small) + ) { + Text(info.positionName, style = typography.labelLarge, fontSize = 20.sp) + Row { + Text( + "Ожидаемая ЗП: ", + style = typography.labelLarge, + fontSize = 18.sp + ) + Text( + "${info.salary}₽", + style = typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + fontSize = 18.sp + ) + } + + } + + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_arr_details), + contentDescription = "Open details" + ) + } + } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt index adbc27e..7df1569 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt @@ -1,143 +1,17 @@ package com.prodhack.moscow2025.presentation.screens.main +import androidx.paging.map +import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase +import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo +import com.prodhack.moscow2025.presentation.dataModels.mapToBaseUIInfo import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import kotlinx.coroutines.flow.map import org.koin.android.annotation.KoinViewModel @KoinViewModel class MainScreenViewModel( -// private val loadTasksUseCase: LoadTasksUseCase, -// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase, -// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase, -// private val changeDeadlineUseCase: ChangeDeadlineUseCase + loadResumeListUseCase: LoadResumeListUseCase ) : BaseViewModel() { - -// var userChanged = false -// -// // Date filter -// private val defaultDateFilterState = -// getStartOfTodayTimestamp().let { Pair(it, it + 86400000) } -// -// -// private val dateState = -// mutableStateOf(defaultDateFilterState) -// -// val dateString = derivedStateOf { -// Log.d( -// "MainScreenViewModel", -// "deriving state , defaultDateFilterState - $defaultDateFilterState" -// ) -// when (dateState.value.first) { -// defaultDateFilterState.first -> "Сегодня" -// defaultDateFilterState.second -> "Завтра" -// else -> timestampToDate(dateState.value.first) -// } + "-" + -// when (dateState.value.second) { -// defaultDateFilterState.first -> "Сегодня" -// defaultDateFilterState.second -> "Завтра" -// else -> timestampToDate(dateState.value.second) -// } -// } -// -// fun setDate(dates: Pair) { -// userChanged = true -// dateState.value = -// Pair( -// convertGMTToSystemTimezone(dates.first), -// convertGMTToSystemTimezone(dates.second) -// ) -// -// Log.d("MainScreenViewModel", "updated dates ${dateState.value}") -// } -// -// // Other -// val onlyMyTasksState = mutableStateOf(true) -// -// val showFinished = mutableStateOf(false) -// -// // Topic filters -// -// val selectedTopicId = mutableStateOf(null) -// -// val topicList = MutableUIStateFlow>() -// -// fun loadTopicList() { -// loadTasksTopicsListUseCase().map { it -> it.map { it -> it.map { it.mapToUI() } } } -// .collectRequest(topicList) -// } -// -// fun selectTopic(id: Int) { -// if (selectedTopicId.value == id) { -// selectedTopicId.value = null -// } else { -// selectedTopicId.value = id -// } -// } -// -// // Tasks -// @OptIn(ExperimentalCoroutinesApi::class) -// val taskList = snapshotFlow { -// val dates = dateState.value -// TaskFilters( -// dateStart = dates.first, -// dateEnd = dates.second, -// topicId = selectedTopicId.value, -// onlySelf = onlyMyTasksState.value, -// showArchived = showFinished.value -// ) -// }.flatMapLatest { -// loadTasksUseCase(it) -// }.map { it -> it.map { it.mapToUI() } } -// -// private val archiveWaitingTaskJobs = mutableStateMapOf() -// -// val archiveWaitingTasksIds = derivedStateOf { archiveWaitingTaskJobs.keys } -// -// fun toggleTaskAsDone(tripId: Long, taskId: Long, currState: Boolean) { -// if (currState) { -// viewModelScope.launch { -// setFinishedStateToTaskUseCase( -// tripId = tripId, -// taskId = taskId, -// finishedState = false -// ) -// } -// } else { -// if (taskId in archiveWaitingTasksIds.value) { -// archiveWaitingTaskJobs[taskId]?.let { job -> -// if (!job.isCompleted) { -// job.cancel() -// } -// } -// archiveWaitingTaskJobs.remove(taskId) -// } else { -// archiveWaitingTaskJobs[taskId] = viewModelScope.launch { -// delay(1000) -// setFinishedStateToTaskUseCase( -// tripId = tripId, -// taskId = taskId, -// finishedState = true -// ) -// }.also { -// it.start() -// } -// } -// } -// } -// -// fun update() { -// loadTopicList() -// } -// -// fun changeTaskDeadline(value: UITaskModel?, date: Long) { -// viewModelScope.launch { -// value?.let { -// changeDeadlineUseCase(value.tripId, value.id, date) -// } -// } -// } -// -// init { -// update() -// } + val resumeList = loadResumeListUseCase().map { it -> it.map { it.mapToBaseUIInfo() } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt index 3074686..deda99a 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState @@ -111,8 +112,8 @@ fun ErrorCollectorScope.RegisterScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), + Icon( + painter = painterResource(R.drawable.app_logo), contentDescription = null, modifier = Modifier .size(200.dp) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt index 09fb7de..f25fdbf 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt @@ -3,6 +3,7 @@ package com.prodhack.moscow2025.presentation.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme @@ -149,6 +150,10 @@ fun MoscowHackatonTemplateTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, + shapes = Shapes( + extraSmall = com.prodhack.moscow2025.presentation.theme.Shapes.verySmallRoundedBox, + small = com.prodhack.moscow2025.presentation.theme.Shapes.smallRoundedBox + ), content = content ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/app_logo.xml b/app/src/main/res/drawable/app_logo.xml new file mode 100644 index 0000000..f46ff69 --- /dev/null +++ b/app/src/main/res/drawable/app_logo.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arr_details.xml b/app/src/main/res/drawable/ic_arr_details.xml new file mode 100644 index 0000000..ac8fc7a --- /dev/null +++ b/app/src/main/res/drawable/ic_arr_details.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74d295c..05850e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - MoscowHackatonTemplate + Rekomenci fluon \ No newline at end of file