You've already forked RekomenciMobile
Merge branch 'master' of gitlab.prodcontest.com:team-39/mobile
* 'master' of gitlab.prodcontest.com:team-39/mobile: feat: added template for resume details screen fix: fix phone field on profile screen, bottom bar beautify; feat: show buttons only after change, on profile edit feat: main screen implemented fix: fixing bugs with phone input field. feat: абсолютно готов экран profile feat: added profile edir screen feat: added view model for profile screen
This commit is contained in:
Vendored
+6
@@ -19,3 +19,9 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
# Keep annotation definitions
|
||||||
|
-keep class org.koin.core.annotation.** { *; }
|
||||||
|
|
||||||
|
# Keep classes annotated with Koin annotations
|
||||||
|
-keep @org.koin.core.annotation.* class * { *; }
|
||||||
+60
-2
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "bf664fe902e116c42af432814d63d6a7",
|
"identityHash": "3e896e9a3d3b2f61149f8c0fde7e5964",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "users",
|
"tableName": "users",
|
||||||
@@ -52,11 +52,69 @@
|
|||||||
"id"
|
"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": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,8 @@ class App : Application() {
|
|||||||
androidContext(this@App)
|
androidContext(this@App)
|
||||||
analytics()
|
analytics()
|
||||||
modules(
|
modules(
|
||||||
listOf(
|
|
||||||
AppModules().module
|
AppModules().module
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
FirebaseApp.initializeApp(this@App)
|
FirebaseApp.initializeApp(this@App)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.prodhack.moscow2025.common
|
package com.prodhack.moscow2025.common
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val BASE_API_URL = "https://hackaton.paas.itqdev.xyz/"
|
const val BASE_API_URL = "https://team-39-alpha-gm5qjkou.hack.prodcontest.ru/"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.prodhack.moscow2025.common.di
|
package com.prodhack.moscow2025.common.di
|
||||||
|
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider
|
import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider
|
||||||
|
import org.koin.core.annotation.Configuration
|
||||||
import org.koin.core.annotation.Module
|
import org.koin.core.annotation.Module
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.prodhack.moscow2025.common.di
|
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.ComponentScan
|
||||||
import org.koin.core.annotation.Module
|
import org.koin.core.annotation.Module
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ComponentScan("com.prodhack.moscow2025.presentation")
|
@ComponentScan("com.prodhack.moscow2025.presentation")
|
||||||
@@ -13,4 +17,13 @@ class DomainModule
|
|||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ComponentScan("com.prodhack.moscow2025.data")
|
@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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.prodhack.moscow2025.data.base
|
|
||||||
|
|
||||||
|
|
||||||
interface BaseEntity {
|
|
||||||
val id: Number
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import androidx.room.RoomDatabase
|
|||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
class BaseRemoteMediator<DBEntity : BaseEntity>(
|
class BaseRemoteMediator<DBEntity : Any>(
|
||||||
private val db: RoomDatabase,
|
private val db: RoomDatabase,
|
||||||
private val dao: BasePaginationDAO<DBEntity>,
|
private val dao: BasePaginationDAO<DBEntity>,
|
||||||
private val makeRequest: suspend (page: Long, pageCount: Int) -> Result<List<DBEntity>>
|
private val makeRequest: suspend (page: Long, pageCount: Int) -> Result<List<DBEntity>>
|
||||||
@@ -26,17 +26,12 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
LoadType.APPEND -> {
|
LoadType.APPEND -> {
|
||||||
val lastItem = state.lastItemOrNull()
|
state.pages.size + 1
|
||||||
if (lastItem == null) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
(lastItem.id.toLong() / state.config.pageSize) + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = makeRequest(
|
val result = makeRequest(
|
||||||
loadKey,
|
(loadKey.toLong() - 1) * state.config.pageSize,
|
||||||
state.config.pageSize
|
state.config.pageSize
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,8 +41,7 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
|
|||||||
if (loadType == LoadType.REFRESH) {
|
if (loadType == LoadType.REFRESH) {
|
||||||
dao.clearAll()
|
dao.clearAll()
|
||||||
}
|
}
|
||||||
val beerEntities = data
|
dao.upsertAll(data)
|
||||||
dao.upsertAll(beerEntities)
|
|
||||||
}
|
}
|
||||||
MediatorResult.Success(
|
MediatorResult.Success(
|
||||||
endOfPaginationReached = data.size < state.config.pageSize
|
endOfPaginationReached = data.size < state.config.pageSize
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ abstract class BaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
protected fun <Value : BaseEntity> paginatedRequest(
|
protected fun <Value : Any> paginatedRequest(
|
||||||
pageSize: Int = 10,
|
pageSize: Int = 10,
|
||||||
prefetchDistance: Int = pageSize,
|
prefetchDistance: Int = pageSize,
|
||||||
enablePlaceholders: Boolean = true,
|
enablePlaceholders: Boolean = true,
|
||||||
@@ -149,7 +149,7 @@ abstract class BaseRepository {
|
|||||||
maxSize: Int = Int.MAX_VALUE,
|
maxSize: Int = Int.MAX_VALUE,
|
||||||
jumpThreshold: Int = Int.MIN_VALUE,
|
jumpThreshold: Int = Int.MIN_VALUE,
|
||||||
dbDao: BasePaginationDAO<Value>,
|
dbDao: BasePaginationDAO<Value>,
|
||||||
makeRequest: suspend (page: Long, pageSize: Int) -> Result<List<Value>>
|
makeRequest: suspend (offset: Long, pageSize: Int) -> Result<List<Value>>
|
||||||
): Flow<PagingData<Value>> {
|
): Flow<PagingData<Value>> {
|
||||||
assertDBSpecify()
|
assertDBSpecify()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.prodhack.moscow2025.data.base
|
|
||||||
|
|
||||||
interface DBMappableDTO <T> {
|
|
||||||
fun mapToDB(): T
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.prodhack.moscow2025.data.base
|
|
||||||
|
|
||||||
interface DomainMappableDTO <T> {
|
|
||||||
fun mapToDomain(): T
|
|
||||||
}
|
|
||||||
+4
-1
@@ -3,11 +3,13 @@ package com.prodhack.moscow2025.data.data_providers.local_db
|
|||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
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.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.dao.UserDao
|
||||||
|
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [UserEntity::class],
|
entities = [UserEntity::class, ResumeEntity::class],
|
||||||
version = 1,
|
version = 1,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@@ -15,4 +17,5 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun userDao(): UserDao
|
abstract fun userDao(): UserDao
|
||||||
|
|
||||||
abstract fun cleanUpDao(): CleanUpDao
|
abstract fun cleanUpDao(): CleanUpDao
|
||||||
|
abstract fun resumeDao(): ResumeDao
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -8,12 +8,5 @@ import org.koin.core.annotation.Single
|
|||||||
@Module
|
@Module
|
||||||
class DatabaseProvider {
|
class DatabaseProvider {
|
||||||
|
|
||||||
@Single
|
|
||||||
fun provideDatabase(context: Context): AppDatabase =
|
|
||||||
Room.databaseBuilder(
|
|
||||||
context,
|
|
||||||
AppDatabase::class.java,
|
|
||||||
"t_tasks.db"
|
|
||||||
).fallbackToDestructiveMigration()
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -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<ResumeEntity> {
|
||||||
|
|
||||||
|
@Query("DELETE FROM resumes")
|
||||||
|
override suspend fun clearAll()
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
override suspend fun upsertAll(data: List<ResumeEntity>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM resumes")
|
||||||
|
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
|
||||||
|
}
|
||||||
+37
@@ -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("|")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,33 +7,6 @@ import com.prodhack.moscow2025.domain.models.User
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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
|
@Serializable
|
||||||
data class UserLoginRequest(
|
data class UserLoginRequest(
|
||||||
@@ -58,28 +31,3 @@ data class TokenResponse(
|
|||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
val token: String
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.prodhack.moscow2025.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ErrorNetworkDTO(
|
||||||
|
val detail: String
|
||||||
|
)
|
||||||
@@ -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<String>,
|
||||||
|
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<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResumeListDTO(
|
||||||
|
val resumes: List<ResumeDTO>
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
+40
@@ -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<ResumeModel> = paginatedRequest(
|
||||||
|
pageSize = 20,
|
||||||
|
dbDao = resumeDao,
|
||||||
|
makeRequest = { offset, pageSize ->
|
||||||
|
networkRequest<ResumeListDTO> {
|
||||||
|
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() } }
|
||||||
|
}
|
||||||
+8
@@ -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<ResumeModel>
|
||||||
|
}
|
||||||
@@ -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<String>,
|
||||||
|
val experienceType: ExperienceType,
|
||||||
|
val prediction: Pair<Int?, Int?>,
|
||||||
|
val recommendedSkills: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ExperienceType {
|
||||||
|
NoExperience,
|
||||||
|
LessThan1,
|
||||||
|
Between1And3,
|
||||||
|
Between3And6,
|
||||||
|
MoreThan6
|
||||||
|
}
|
||||||
+10
-1
@@ -1,5 +1,6 @@
|
|||||||
package com.prodhack.moscow2025.domain.usecase.auth
|
package com.prodhack.moscow2025.domain.usecase.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.prodhack.moscow2025.domain.interfaces.AuthRepository
|
import com.prodhack.moscow2025.domain.interfaces.AuthRepository
|
||||||
import com.prodhack.moscow2025.domain.interfaces.UserRepository
|
import com.prodhack.moscow2025.domain.interfaces.UserRepository
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
@@ -16,14 +17,22 @@ class CheckSessionUseCase(
|
|||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val userRepository: UserRepository
|
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 =
|
suspend operator fun invoke(): SessionState =
|
||||||
if (authRepository.fetchLoginState().firstOrNull() == true) {
|
if (authRepository.fetchLoginState().firstOrNull() == true) {
|
||||||
|
Log.d(TAG, "user authorized, requesting profile")
|
||||||
if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) {
|
if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) {
|
||||||
|
Log.d(TAG, "user authorized, first name is blank -> need fill profile")
|
||||||
SessionState.NotFilledProfile
|
SessionState.NotFilledProfile
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "user authorized, first name is filled -> user already fill profile")
|
||||||
SessionState.FilledAndAuthorized
|
SessionState.FilledAndAuthorized
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+19
@@ -24,6 +24,25 @@ data class ValidationResult(
|
|||||||
|
|
||||||
@Single
|
@Single
|
||||||
class ValidateAuthFieldsUseCase {
|
class ValidateAuthFieldsUseCase {
|
||||||
|
fun validateProfile(
|
||||||
|
chosenPattern: PhoneNumberPattern?,
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
email: String,
|
||||||
|
phone: String
|
||||||
|
): ValidationResult {
|
||||||
|
val errors = buildMap {
|
||||||
|
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||||
|
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||||
|
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||||
|
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||||
|
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||||
|
AuthField.Phone,
|
||||||
|
"Некорректный номер телефона"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ValidationResult(errors)
|
||||||
|
}
|
||||||
|
|
||||||
fun validateFillProfile(
|
fun validateFillProfile(
|
||||||
chosenPattern: PhoneNumberPattern?,
|
chosenPattern: PhoneNumberPattern?,
|
||||||
|
|||||||
+40
@@ -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<ResumeModel> = resumeRepository.loadResumeList()
|
||||||
|
|
||||||
|
// Mocked data
|
||||||
|
operator fun invoke(): RemotePagingWrapper<ResumeModel> = 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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,10 @@ import kotlin.getValue
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "MainActivity"
|
||||||
|
}
|
||||||
|
|
||||||
private val checkSessionUseCase: CheckSessionUseCase by inject()
|
private val checkSessionUseCase: CheckSessionUseCase by inject()
|
||||||
|
|
||||||
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
|
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
|
||||||
@@ -42,8 +46,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val sessionState = try {
|
val sessionState = try {
|
||||||
checkSessionUseCase()
|
checkSessionUseCase().also {
|
||||||
|
Log.d(TAG, "SessionState received $it")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in session state getting process", e)
|
||||||
SessionState.NotAuthorized
|
SessionState.NotAuthorized
|
||||||
}
|
}
|
||||||
sessionDestinationState.value =
|
sessionDestinationState.value =
|
||||||
@@ -67,7 +74,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
.addOnCompleteListener { task ->
|
.addOnCompleteListener { task ->
|
||||||
if (task.isSuccessful) {
|
if (task.isSuccessful) {
|
||||||
val token = task.result
|
val token = task.result
|
||||||
Log.d("TOKEN", token)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,6 +4,8 @@ import android.util.Log
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -59,7 +61,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
|||||||
}
|
}
|
||||||
target?.let { (it - center).toDp() }
|
target?.let { (it - center).toDp() }
|
||||||
}
|
}
|
||||||
AnimatedVisibility(indicatorOffset != null) {
|
|
||||||
indicatorOffset?.let {
|
indicatorOffset?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -71,7 +72,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.components.standart
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TPhoneField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
currentPattern: UIPhoneNumberPattern?,
|
||||||
|
currentPhone: String,
|
||||||
|
onPhoneChange: (String) -> Unit,
|
||||||
|
error: String?,
|
||||||
|
onOpenCountryList: () -> Unit,
|
||||||
|
) {
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
Row(
|
||||||
|
modifier = modifier.height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
Paddings.medium
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
FieldWrapper(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(IntrinsicSize.Min)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.offset(y = 5.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.background(colorScheme.primary, Shapes.smallRoundedBox)
|
||||||
|
.clip(Shapes.smallRoundedBox),
|
||||||
|
value = currentPattern?.prefix ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = colorScheme.onPrimary
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
onOpenCountryList()
|
||||||
|
}
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_arr_dropdown),
|
||||||
|
tint = colorScheme.onPrimary,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TTTextField(
|
||||||
|
value = currentPhone,
|
||||||
|
onValueChange = onPhoneChange,
|
||||||
|
label = "Ваш телефон",
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Phone
|
||||||
|
),
|
||||||
|
visualTransformation = currentPattern?.let {
|
||||||
|
PhoneVisualTransformation(
|
||||||
|
it.pattern,
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
} ?: VisualTransformation.None,
|
||||||
|
error = error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TPhoneCountryList(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isSheetOpen: MutableState<Boolean>,
|
||||||
|
sheetState: SheetState,
|
||||||
|
patternList: List<UIPhoneNumberPattern>,
|
||||||
|
setPattern: (UIPhoneNumberPattern) -> Unit
|
||||||
|
) {
|
||||||
|
if (isSheetOpen.value) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier,
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = {
|
||||||
|
isSheetOpen.value = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(patternList) { pattern ->
|
||||||
|
Text(
|
||||||
|
text = pattern.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
.clickable {
|
||||||
|
setPattern(pattern)
|
||||||
|
isSheetOpen.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -28,17 +29,17 @@ fun TopLogo(
|
|||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
modifier = Modifier.size(100.dp),
|
modifier = Modifier.size(100.dp),
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
painter = painterResource(R.drawable.app_logo),
|
||||||
contentDescription = "App logo"
|
contentDescription = "App logo"
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(Paddings.medium))
|
Spacer(modifier = Modifier.width(Paddings.medium))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.app_name),
|
text = stringResource(R.string.app_name),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontSize = 48.sp
|
fontSize = 24.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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() ?: "Ошибка"
|
||||||
|
)
|
||||||
@@ -16,4 +16,9 @@ sealed class AppDestination(val route: String) {
|
|||||||
data object Profile : AppDestination("app/profile")
|
data object Profile : AppDestination("app/profile")
|
||||||
|
|
||||||
data object FillProfile : AppDestination("app/fill_profile")
|
data object FillProfile : AppDestination("app/fill_profile")
|
||||||
|
|
||||||
|
data object ResumeDetails : AppDestination("resume/details") {
|
||||||
|
const val ARG_ID = "id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
import com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
||||||
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.AppSnackbarVisuals
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TTasksApp(
|
fun TTasksApp(
|
||||||
@@ -26,7 +28,7 @@ fun TTasksApp(
|
|||||||
context: Context,
|
context: Context,
|
||||||
sessionDestination: AppDestination? = null
|
sessionDestination: AppDestination? = null
|
||||||
) {
|
) {
|
||||||
MoscowHackatonTemplateTheme() {
|
MoscowHackatonTemplateTheme {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val bottomBarState = remember { mutableStateOf<Int?>(null) }
|
val bottomBarState = remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
@@ -53,10 +55,19 @@ fun TTasksApp(
|
|||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
hostState = snackbarHostState,
|
hostState = snackbarHostState,
|
||||||
snackbar = { data ->
|
snackbar = { data ->
|
||||||
|
val style = (data.visuals as? AppSnackbarVisuals)?.style ?: SnackbarStyle.Error
|
||||||
|
val containerColor = when (style) {
|
||||||
|
SnackbarStyle.Success -> MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
SnackbarStyle.Error -> MaterialTheme.colorScheme.errorContainer
|
||||||
|
}
|
||||||
|
val contentColor = when (style) {
|
||||||
|
SnackbarStyle.Success -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
SnackbarStyle.Error -> MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
Snackbar(
|
Snackbar(
|
||||||
snackbarData = data,
|
snackbarData = data,
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
containerColor = containerColor,
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
contentColor = contentColor,
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.prodhack.moscow2025.presentation.navigation
|
package com.prodhack.moscow2025.presentation.navigation
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -13,6 +15,7 @@ import com.prodhack.moscow2025.presentation.screens.fillProfile.FillProfileScree
|
|||||||
import com.prodhack.moscow2025.presentation.screens.login.LoginScreen
|
import com.prodhack.moscow2025.presentation.screens.login.LoginScreen
|
||||||
import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen
|
import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen
|
||||||
import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
|
import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
|
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||||
@@ -88,12 +91,25 @@ fun TTasksNavHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable(AppDestination.Main.route) {
|
composable(AppDestination.Main.route) {
|
||||||
MainScreen()
|
MainScreen(openResumeDetails = { id ->
|
||||||
|
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
|
||||||
|
putString(AppDestination.ResumeDetails.ARG_ID, id)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(AppDestination.Profile.route)
|
composable(AppDestination.Profile.route)
|
||||||
{
|
{
|
||||||
ProfileScreen()
|
ProfileScreen(
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
navigateToLoginScreen = {
|
||||||
|
navController.navigate(AppDestination.Login.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppDestination.ResumeDetails.route) {
|
||||||
|
ResumeDetailsScreen(navBackStackEntry = it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.navigation
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.Navigator
|
||||||
|
|
||||||
|
fun NavController.navigate(
|
||||||
|
route: String,
|
||||||
|
args: Bundle
|
||||||
|
) {
|
||||||
|
val nodeId = graph.findNode(route = route)?.id
|
||||||
|
if (nodeId != null) {
|
||||||
|
navigate(nodeId, args, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-91
@@ -44,7 +44,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -56,6 +55,8 @@ import com.prodhack.moscow2025.R
|
|||||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
@@ -144,11 +145,10 @@ fun ErrorCollectorScope.FillProfileScreen(
|
|||||||
style = typography.titleLarge,
|
style = typography.titleLarge,
|
||||||
fontSize = 31.sp
|
fontSize = 31.sp
|
||||||
)
|
)
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
painter = painterResource(R.drawable.app_logo),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(140.dp),
|
modifier = Modifier.size(140.dp)
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
@@ -166,70 +166,16 @@ fun ErrorCollectorScope.FillProfileScreen(
|
|||||||
error = formState.errors[AuthField.LastName],
|
error = formState.errors[AuthField.LastName],
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
TPhoneField(
|
||||||
Row(
|
currentPattern = viewModel.currentPattern.value,
|
||||||
modifier = Modifier.height(IntrinsicSize.Min),
|
currentPhone = formState.phone,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onPhoneChange = viewModel::onPhoneChange,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
error = formState.errors[AuthField.Phone],
|
||||||
Paddings.medium
|
onOpenCountryList =
|
||||||
)
|
{
|
||||||
) {
|
|
||||||
FieldWrapper(modifier = Modifier
|
|
||||||
.width(IntrinsicSize.Min)
|
|
||||||
.fillMaxHeight()) {
|
|
||||||
BasicTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.offset(y = 5.dp)
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.background(colorScheme.primary, Shapes.smallRoundedBox)
|
|
||||||
.clip(Shapes.smallRoundedBox),
|
|
||||||
value = viewModel.chosenPattern.value?.prefix ?: "",
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = colorScheme.onPrimary
|
|
||||||
),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
isSheetOpen.value = true
|
isSheetOpen.value = true
|
||||||
}
|
}
|
||||||
.padding(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(15.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_arr_dropdown),
|
|
||||||
tint = colorScheme.onPrimary,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TTTextField(
|
|
||||||
value = formState.phone,
|
|
||||||
onValueChange = viewModel::onPhoneChange,
|
|
||||||
label = "Ваш телефон",
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Phone
|
|
||||||
),
|
|
||||||
visualTransformation = viewModel.chosenPattern.value?.pattern?.let {
|
|
||||||
PhoneVisualTransformation(
|
|
||||||
it,
|
|
||||||
'0'
|
|
||||||
)
|
|
||||||
} ?: VisualTransformation.None,
|
|
||||||
error = formState.errors[AuthField.Phone]
|
|
||||||
)
|
|
||||||
Log.d("Test", formState.errors[AuthField.Phone].toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
BigButton(
|
BigButton(
|
||||||
@@ -242,32 +188,12 @@ fun ErrorCollectorScope.FillProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSheetOpen.value) {
|
TPhoneCountryList(
|
||||||
ModalBottomSheet(
|
isSheetOpen = isSheetOpen,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = {
|
patternList = viewModel.phoneNumberPatterns,
|
||||||
isSheetOpen.value = false
|
setPattern = {
|
||||||
},
|
viewModel.currentPattern.value = it
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
||||||
items(viewModel.phoneNumberPatterns) { pattern ->
|
|
||||||
Text(
|
|
||||||
text = pattern.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp)
|
|
||||||
.clickable {
|
|
||||||
viewModel.chosenPattern.value = pattern
|
|
||||||
isSheetOpen.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+7
-6
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Single
|
|
||||||
|
|
||||||
data class FillProfileFormState(
|
data class FillProfileFormState(
|
||||||
val firstName: String = "",
|
val firstName: String = "",
|
||||||
@@ -95,9 +94,11 @@ class FillProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onPhoneChange(value: String) {
|
fun onPhoneChange(value: String) {
|
||||||
|
val maxDigits = currentPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
||||||
_formStateFillProfile.update {
|
_formStateFillProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
phone = value,
|
phone = digits,
|
||||||
errors = it.errors - AuthField.Phone
|
errors = it.errors - AuthField.Phone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -151,14 +152,14 @@ class FillProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
val currentPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||||
|
|
||||||
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||||
|
|
||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
// Load default pattern
|
// Load default pattern
|
||||||
chosenPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
currentPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
|
|
||||||
// Load all phone number patterns
|
// Load all phone number patterns
|
||||||
phoneNumberPatterns.clear()
|
phoneNumberPatterns.clear()
|
||||||
@@ -171,7 +172,7 @@ class FillProfileViewModel(
|
|||||||
firstName = _formStateFillProfile.value.firstName,
|
firstName = _formStateFillProfile.value.firstName,
|
||||||
lastName = _formStateFillProfile.value.lastName,
|
lastName = _formStateFillProfile.value.lastName,
|
||||||
phone = _formStateFillProfile.value.phone,
|
phone = _formStateFillProfile.value.phone,
|
||||||
chosenPattern = chosenPattern.value?.mapToDomain()
|
chosenPattern = currentPattern.value?.mapToDomain()
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
@@ -185,7 +186,7 @@ class FillProfileViewModel(
|
|||||||
UpdateUserData(
|
UpdateUserData(
|
||||||
firstName = _formStateFillProfile.value.firstName,
|
firstName = _formStateFillProfile.value.firstName,
|
||||||
lastName = _formStateFillProfile.value.lastName,
|
lastName = _formStateFillProfile.value.lastName,
|
||||||
phone = chosenPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
phone = currentPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
||||||
convertNumberToPattern(
|
convertNumberToPattern(
|
||||||
phoneNumberPattern,
|
phoneNumberPattern,
|
||||||
_formStateFillProfile.value.phone
|
_formStateFillProfile.value.phone
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -127,8 +128,8 @@ fun ErrorCollectorScope.LoginScreen(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
painter = painterResource(R.drawable.app_logo),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(200.dp)
|
.size(200.dp)
|
||||||
|
|||||||
+162
-263
@@ -1,9 +1,39 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.main
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@@ -12,271 +42,140 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun ErrorCollectorScope.MainScreen(
|
fun ErrorCollectorScope.MainScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
openResumeDetails: (String) -> Unit,
|
||||||
viewModel: MainScreenViewModel = koinViewModel()
|
viewModel: MainScreenViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
Text("Main screen will be here soon")
|
val typography = MaterialTheme.typography
|
||||||
// val openCalendarModal = remember { mutableStateOf(false) }
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
// val openTaskAddSheet = remember { mutableStateOf(false) }
|
val shapes = MaterialTheme.shapes
|
||||||
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
// val tasks = viewModel.taskList.collectAsLazyPagingItems()
|
Box {
|
||||||
//
|
Column(
|
||||||
// val selectedTask = remember { mutableStateOf<UITaskModel?>(null) }
|
modifier = modifier
|
||||||
//
|
.fillMaxSize()
|
||||||
// Box(
|
.padding(horizontal = 20.dp),
|
||||||
// modifier = modifier
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
// .fillMaxSize()
|
) {
|
||||||
// .padding(horizontal = Paddings.large),
|
TopLogo()
|
||||||
// contentAlignment = Alignment.BottomCenter
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
// ) {
|
Text(
|
||||||
// Column(
|
text = "Ваши резюме",
|
||||||
// modifier = Modifier.fillMaxSize(),
|
style = typography.titleLarge,
|
||||||
// horizontalAlignment = Alignment.CenterHorizontally
|
fontSize = 32.sp,
|
||||||
// ) {
|
color = colorScheme.onBackground
|
||||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
)
|
||||||
// TopLogo()
|
|
||||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
//
|
|
||||||
// MainScreenFilters(viewModel = viewModel) {
|
val items = viewModel.resumeList.collectAsLazyPagingItems()
|
||||||
// openCalendarModal.value = true
|
|
||||||
// }
|
if (items.itemCount == 0 && items.loadState.append.endOfPaginationReached) {
|
||||||
//
|
Text(
|
||||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
text = "Здесь пока ничего нет",
|
||||||
//
|
style = typography.labelLarge,
|
||||||
// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics ->
|
textAlign = TextAlign.Center,
|
||||||
// BubbledCategoryFilters(
|
fontSize = 24.sp,
|
||||||
// categories = topics,
|
color = colorScheme.onBackground
|
||||||
// selectedItemId = viewModel.selectedTopicId.value ?: -1
|
)
|
||||||
// ) { categoryId ->
|
|
||||||
// viewModel.selectTopic(categoryId)
|
BigButton(onClick = {
|
||||||
// }
|
TODO()
|
||||||
// }
|
}, buttonText = "Создать резюме", isLoading = false)
|
||||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
} else if (items.loadState.hasError) {
|
||||||
//
|
Text(
|
||||||
// if (tasks.loadState.hasError) {
|
modifier = Modifier
|
||||||
// Text(
|
.fillMaxWidth()
|
||||||
// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!",
|
.background(colorScheme.error, shape = shapes.small)
|
||||||
// style = Typography.titleMedium,
|
.padding(Paddings.medium),
|
||||||
// textAlign = TextAlign.Center,
|
text = "Кажется что-то пошло не так, но мы уже чиним 🛠️",
|
||||||
// fontSize = 18.sp,
|
style = typography.labelLarge,
|
||||||
// color = MaterialTheme.colorScheme.error
|
textAlign = TextAlign.Center,
|
||||||
// )
|
fontSize = 24.sp,
|
||||||
// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) {
|
color = colorScheme.onError
|
||||||
// Spacer(modifier = Modifier.weight(1f))
|
)
|
||||||
//
|
} else {
|
||||||
// Text(
|
LazyColumn(
|
||||||
// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку",
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
// style = Typography.titleMedium,
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
// textAlign = TextAlign.Center,
|
Paddings.medium
|
||||||
// fontSize = 18.sp,
|
)
|
||||||
// color = MaterialTheme.colorScheme.onBackground
|
) {
|
||||||
// )
|
items(items.itemCount) {
|
||||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
items[it]?.let { resume ->
|
||||||
// BigButton(buttonText = "Начать", onClick = {
|
ResumeShortInfoCard(info = resume) {
|
||||||
//
|
openResumeDetails(resume.id)
|
||||||
// }, isLoading = false)
|
}
|
||||||
//
|
}
|
||||||
// Spacer(modifier = Modifier.weight(3f))
|
}
|
||||||
//
|
|
||||||
// } else {
|
item {
|
||||||
// LazyColumn(
|
if (items.loadState.append.endOfPaginationReached.not()) {
|
||||||
// verticalArrangement = Arrangement.spacedBy(Paddings.small),
|
CircularProgressIndicator()
|
||||||
// horizontalAlignment = Alignment.CenterHorizontally
|
}
|
||||||
// ) {
|
}
|
||||||
// items(tasks.itemCount) { it ->
|
}
|
||||||
// val task = tasks[it]
|
}
|
||||||
// task?.let {
|
}
|
||||||
// TaskCard(
|
|
||||||
// onClick = {
|
val context = LocalContext.current
|
||||||
// selectedTask.value = it
|
TTFloatingActionButton(
|
||||||
// },
|
modifier = Modifier
|
||||||
// taskInfo = it,
|
.align(Alignment.BottomCenter)
|
||||||
// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value
|
.padding(bottom = Paddings.medium),
|
||||||
// ) {
|
onClick = {
|
||||||
// viewModel.toggleTaskAsDone(
|
Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show()
|
||||||
// tripId = it.tripId,
|
},
|
||||||
// taskId = it.id,
|
text = "Добавить резюме"
|
||||||
// currState = it.archived
|
)
|
||||||
// )
|
}
|
||||||
// tasks.refresh()
|
}
|
||||||
// }
|
|
||||||
// }
|
@Composable
|
||||||
// }
|
fun ResumeShortInfoCard(
|
||||||
//
|
modifier: Modifier = Modifier,
|
||||||
// item {
|
info: UIResumeBaseInfo,
|
||||||
// if (!tasks.loadState.append.endOfPaginationReached) {
|
onClick: () -> Unit
|
||||||
// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
) {
|
||||||
// }
|
val typography = MaterialTheme.typography
|
||||||
// }
|
Card(
|
||||||
// }
|
modifier = modifier.fillMaxWidth(),
|
||||||
// }
|
shape = MaterialTheme.shapes.small,
|
||||||
// }
|
onClick = onClick
|
||||||
//
|
) {
|
||||||
// TTFloatingActionButton(
|
Row(
|
||||||
// modifier = Modifier
|
modifier = Modifier
|
||||||
// .align(Alignment.BottomCenter)
|
.fillMaxWidth()
|
||||||
// .padding(bottom = Paddings.medium),
|
.padding(Paddings.medium),
|
||||||
// onClick = {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
// openTaskAddSheet.value = true
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
// },
|
) {
|
||||||
// text = "Добавить задачу"
|
Column(
|
||||||
// )
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
// }
|
) {
|
||||||
//
|
Text(info.positionName, style = typography.labelLarge, fontSize = 20.sp)
|
||||||
//
|
Row {
|
||||||
// AnimatedVisibility(openCalendarModal.value) {
|
Text(
|
||||||
// DateRangePickerModal({
|
"Ожидаемая ЗП: ",
|
||||||
// Log.d("DatePicker", it.toString())
|
style = typography.labelLarge,
|
||||||
// if (it.first != null && it.second != null) {
|
fontSize = 18.sp
|
||||||
// viewModel.setDate(Pair(it.first!!, it.second!!))
|
)
|
||||||
// openCalendarModal.value = false
|
Text(
|
||||||
// }
|
"${info.salary}₽",
|
||||||
// }) {
|
style = typography.titleMedium,
|
||||||
// openCalendarModal.value = false
|
color = MaterialTheme.colorScheme.primary,
|
||||||
// }
|
fontSize = 18.sp
|
||||||
// }
|
)
|
||||||
//
|
}
|
||||||
// if (openTaskAddSheet.value) {
|
|
||||||
// AddTaskBottomSheet(
|
}
|
||||||
// sheetState = sheetState,
|
|
||||||
// onDismiss = {
|
Icon(
|
||||||
// openTaskAddSheet.value = false
|
modifier = Modifier.size(24.dp),
|
||||||
// }
|
painter = painterResource(R.drawable.ic_arr_details),
|
||||||
// )
|
contentDescription = "Open details"
|
||||||
// }
|
)
|
||||||
//
|
}
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-133
@@ -1,143 +1,17 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.main
|
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 com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
|
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class MainScreenViewModel(
|
class MainScreenViewModel(
|
||||||
// private val loadTasksUseCase: LoadTasksUseCase,
|
loadResumeListUseCase: LoadResumeListUseCase
|
||||||
// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase,
|
|
||||||
// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase,
|
|
||||||
// private val changeDeadlineUseCase: ChangeDeadlineUseCase
|
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
val resumeList = loadResumeListUseCase().map { it -> it.map { it.mapToBaseUIInfo() } }
|
||||||
// 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 <dateString>, 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<Long, Long>) {
|
|
||||||
// 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<Int?>(null)
|
|
||||||
//
|
|
||||||
// val topicList = MutableUIStateFlow<List<UITaskTopicModel>>()
|
|
||||||
//
|
|
||||||
// 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<Long, Job>()
|
|
||||||
//
|
|
||||||
// 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()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+231
-1
@@ -1,9 +1,239 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.profile
|
package com.prodhack.moscow2025.presentation.screens.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonColors
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(modifier: Modifier = Modifier) {
|
fun ErrorCollectorScope.ProfileScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
navigateToLoginScreen: () -> Unit,
|
||||||
|
viewModel: ProfileScreenViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
val typography = androidx.compose.material3.MaterialTheme.typography
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val isSheetOpen = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val formState by viewModel.formStateFillProfile.collectAsState()
|
||||||
|
|
||||||
|
var errorText by remember { mutableStateOf("") }
|
||||||
|
val profileState by viewModel.profileState.collectAsStateWithCallbacks(
|
||||||
|
onInputError = {
|
||||||
|
errorText = it.error
|
||||||
|
},
|
||||||
|
onConnectionError = {
|
||||||
|
errorText = "Нет подключения к сети"
|
||||||
|
},
|
||||||
|
onUnexpectedError = {
|
||||||
|
errorText = it.error
|
||||||
|
},
|
||||||
|
onLoading = {
|
||||||
|
errorText = ""
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
errorText = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(profileState) {
|
||||||
|
if (profileState is UIState.Success) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = "Данные профиля обновлены",
|
||||||
|
style = SnackbarStyle.Success,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(errorText) {
|
||||||
|
if (errorText.isNotEmpty()) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = "Ошибка: $errorText",
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.systemBarsPadding()
|
||||||
|
.padding(horizontal = Paddings.large),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(Paddings.large))
|
||||||
|
Text(
|
||||||
|
text = "Профиль",
|
||||||
|
style = typography.titleLarge,
|
||||||
|
fontSize = 40.sp
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Paddings.large))
|
||||||
|
TTTextField(
|
||||||
|
value = formState.firstName,
|
||||||
|
onValueChange = viewModel::onFirstNameChange,
|
||||||
|
label = "Имя",
|
||||||
|
error = formState.errors[AuthField.FirstName],
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
TTTextField(
|
||||||
|
value = formState.lastName,
|
||||||
|
onValueChange = viewModel::onLastNameChange,
|
||||||
|
label = "Фамилия",
|
||||||
|
error = formState.errors[AuthField.LastName],
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
TTTextField(
|
||||||
|
value = formState.email,
|
||||||
|
onValueChange = viewModel::onEmailChange,
|
||||||
|
label = "Email",
|
||||||
|
error = formState.errors[AuthField.Email],
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
TPhoneField(
|
||||||
|
currentPattern = viewModel.chosenPattern.value,
|
||||||
|
currentPhone = formState.phone,
|
||||||
|
onPhoneChange = viewModel::onPhoneChange,
|
||||||
|
error = formState.errors[AuthField.Phone],
|
||||||
|
onOpenCountryList = {
|
||||||
|
isSheetOpen.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
if (viewModel.madeChanges.collectAsState().value) {
|
||||||
|
BigButton(
|
||||||
|
onClick = viewModel::submit,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
buttonText = "Сохранить",
|
||||||
|
isLoading = profileState is UIState.Loading
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
Button(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = viewModel::reset,
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = colorScheme.secondaryContainer,
|
||||||
|
contentColor = colorScheme.onSecondaryContainer,
|
||||||
|
disabledContainerColor = colorScheme.secondaryContainer,
|
||||||
|
disabledContentColor = colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Отменить",
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(Paddings.medium))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = {
|
||||||
|
viewModel.logout()
|
||||||
|
navigateToLoginScreen()
|
||||||
|
},
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = colorScheme.errorContainer,
|
||||||
|
contentColor = colorScheme.onErrorContainer,
|
||||||
|
disabledContainerColor = colorScheme.errorContainer,
|
||||||
|
disabledContentColor = colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Выйти из аккаунта",
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(Paddings.small))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.logout_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
}
|
||||||
|
|
||||||
|
TPhoneCountryList(
|
||||||
|
isSheetOpen = isSheetOpen,
|
||||||
|
sheetState = sheetState,
|
||||||
|
patternList = viewModel.phoneNumberPatterns,
|
||||||
|
setPattern = {
|
||||||
|
viewModel.chosenPattern.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.screens.profile
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.map
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
|
||||||
|
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
||||||
|
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.GetUserUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.annotation.KoinViewModel
|
||||||
|
|
||||||
|
data class ProfileState(
|
||||||
|
val email: String = "",
|
||||||
|
val firstName: String = "",
|
||||||
|
val lastName: String = "",
|
||||||
|
val phone: String = "",
|
||||||
|
val avatar: ByteArray? = null,
|
||||||
|
val errors: Map<AuthField, String> = emptyMap()
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ProfileState
|
||||||
|
|
||||||
|
if (email != other.email) return false
|
||||||
|
if (firstName != other.firstName) return false
|
||||||
|
if (lastName != other.lastName) return false
|
||||||
|
if (phone != other.phone) return false
|
||||||
|
if (!avatar.contentEquals(other.avatar)) return false
|
||||||
|
if (errors != other.errors) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = email.hashCode()
|
||||||
|
result = 31 * result + firstName.hashCode()
|
||||||
|
result = 31 * result + lastName.hashCode()
|
||||||
|
result = 31 * result + phone.hashCode()
|
||||||
|
result = 31 * result + (avatar?.contentHashCode() ?: 0)
|
||||||
|
result = 31 * result + errors.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@KoinViewModel
|
||||||
|
class ProfileScreenViewModel(
|
||||||
|
private val getUserUseCase: GetUserUseCase,
|
||||||
|
private val updateUserUseCase: UpdateUserUseCase,
|
||||||
|
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||||
|
private val logOutUseCase: LogOutUseCase,
|
||||||
|
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||||
|
galleryRepository: GalleryRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
private val _formStateProfile = MutableStateFlow(ProfileState())
|
||||||
|
val formStateFillProfile: StateFlow<ProfileState> = _formStateProfile
|
||||||
|
|
||||||
|
private val _profileState = MutableUIStateFlow<String>()
|
||||||
|
val profileState: StateFlow<UIState<String>> = _profileState
|
||||||
|
|
||||||
|
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||||
|
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||||
|
|
||||||
|
private val realState = MutableStateFlow(_formStateProfile.value)
|
||||||
|
|
||||||
|
val madeChanges = _formStateProfile.combine(realState) { current, real ->
|
||||||
|
current.phone != real.phone ||
|
||||||
|
current.firstName != real.firstName ||
|
||||||
|
current.lastName != real.lastName ||
|
||||||
|
current.email != real.email
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
if (madeChanges.value) {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
email = realState.value.email,
|
||||||
|
phone = realState.value.phone,
|
||||||
|
firstName = realState.value.firstName,
|
||||||
|
lastName = realState.value.lastName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmailChange(value: String) {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
email = value,
|
||||||
|
errors = it.errors - AuthField.Email
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFirstNameChange(value: String) {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
firstName = value,
|
||||||
|
errors = it.errors - AuthField.FirstName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLastNameChange(value: String) {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
lastName = value,
|
||||||
|
errors = it.errors - AuthField.LastName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPhoneChange(value: String) {
|
||||||
|
val maxDigits = chosenPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
phone = digits,
|
||||||
|
errors = it.errors - AuthField.Phone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val galleryItems = galleryRepository.getImagesIds().map {
|
||||||
|
it.map { id ->
|
||||||
|
ContentUris.withAppendedId(
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun post(context: Context) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
post(
|
||||||
|
(ImageLoader(context).execute(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(currentPhoto).build()
|
||||||
|
).drawable as BitmapDrawable).bitmap
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun post(bitmap: Bitmap) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
avatar = bitmap.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAvatar() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
avatar = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPhoto: Uri? = null
|
||||||
|
|
||||||
|
fun selectImage(photo: Uri) {
|
||||||
|
currentPhoto = photo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submit() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val pattern = chosenPattern.value
|
||||||
|
val validation = validateAuthFieldsUseCase.validateProfile(
|
||||||
|
chosenPattern = pattern?.mapToDomain(),
|
||||||
|
firstName = _formStateProfile.value.firstName,
|
||||||
|
lastName = _formStateProfile.value.lastName,
|
||||||
|
email = _formStateProfile.value.email,
|
||||||
|
phone = _formStateProfile.value.phone
|
||||||
|
)
|
||||||
|
|
||||||
|
val errors = validation.errors.toMutableMap()
|
||||||
|
|
||||||
|
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
_formStateProfile.update { it.copy(errors = errors) }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
_profileState.emit(UIState.Loading())
|
||||||
|
|
||||||
|
val formattedPhone = pattern?.mapToDomain()?.let { phonePattern ->
|
||||||
|
convertNumberToPattern(phonePattern, _formStateProfile.value.phone)
|
||||||
|
} ?: _formStateProfile.value.phone
|
||||||
|
|
||||||
|
val result = updateUserUseCase(
|
||||||
|
UpdateUserData(
|
||||||
|
firstName = _formStateProfile.value.firstName,
|
||||||
|
lastName = _formStateProfile.value.lastName,
|
||||||
|
email = _formStateProfile.value.email,
|
||||||
|
phone = formattedPhone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result.map { it.id }.collectRequest(_profileState)
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
logOutUseCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadPhonePatterns()
|
||||||
|
|
||||||
|
val user = getUserUseCase().getOrNull()
|
||||||
|
if (user != null) {
|
||||||
|
val digits = user.phone.orEmpty().filter { it.isDigit() }
|
||||||
|
val selectedPattern = phoneNumberPatterns.firstOrNull { pattern ->
|
||||||
|
val codeDigits = pattern.countryCode.filter { it.isDigit() }
|
||||||
|
digits.startsWith(codeDigits) && digits.length >= codeDigits.length
|
||||||
|
} ?: getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
|
?: phoneNumberPatterns.firstOrNull()
|
||||||
|
|
||||||
|
selectedPattern?.let { chosenPattern.value = it }
|
||||||
|
|
||||||
|
val digitsWithoutCode = selectedPattern?.let {
|
||||||
|
val codeDigits = it.countryCode.filter { d -> d.isDigit() }
|
||||||
|
if (digits.startsWith(codeDigits)) digits.drop(codeDigits.length) else digits
|
||||||
|
} ?: digits
|
||||||
|
|
||||||
|
val maxDigits = selectedPattern?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
|
||||||
|
_formStateProfile.update {
|
||||||
|
it.copy(
|
||||||
|
firstName = user.firstName.orEmpty(),
|
||||||
|
lastName = user.lastName.orEmpty(),
|
||||||
|
email = user.email,
|
||||||
|
phone = digitsWithoutCode.take(maxDigits)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
realState.emit(_formStateProfile.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPhonePatterns() {
|
||||||
|
phoneNumberPatterns.clear()
|
||||||
|
phoneNumberPatterns.addAll(
|
||||||
|
PhoneNumberPatternsProvider.phoneNumberPatterns.map { it.mapToUI() }
|
||||||
|
)
|
||||||
|
if (chosenPattern.value == null) {
|
||||||
|
val defaultPattern = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
|
chosenPattern.value = defaultPattern ?: phoneNumberPatterns.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -111,8 +112,8 @@ fun ErrorCollectorScope.RegisterScreen(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
painter = painterResource(R.drawable.app_logo),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(200.dp)
|
.size(200.dp)
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ResumeDetailsScreen(
|
||||||
|
navBackStackEntry: NavBackStackEntry,
|
||||||
|
viewModel: ResumeDetailsViewModel = koinViewModel {
|
||||||
|
parametersOf(
|
||||||
|
navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text("Opened resume details for id ${navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""}")
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import org.koin.android.annotation.KoinViewModel
|
||||||
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
|
@KoinViewModel
|
||||||
|
class ResumeDetailsViewModel(
|
||||||
|
@Provided resumeId: String
|
||||||
|
) : BaseViewModel() {
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.prodhack.moscow2025.presentation.theme
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
@@ -149,6 +150,10 @@ fun MoscowHackatonTemplateTheme(
|
|||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
|
shapes = Shapes(
|
||||||
|
extraSmall = com.prodhack.moscow2025.presentation.theme.Shapes.verySmallRoundedBox,
|
||||||
|
small = com.prodhack.moscow2025.presentation.theme.Shapes.smallRoundedBox
|
||||||
|
),
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+31
-8
@@ -29,7 +29,19 @@ class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : Visual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransformedText(annotatedString, PhoneOffsetMapper(mask, maskNumber))
|
if (annotatedString.isEmpty()) {
|
||||||
|
return TransformedText(annotatedString, OffsetMapping.Identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransformedText(
|
||||||
|
annotatedString,
|
||||||
|
PhoneOffsetMapper(
|
||||||
|
mask = mask,
|
||||||
|
numberChar = maskNumber,
|
||||||
|
transformedLength = annotatedString.length,
|
||||||
|
maxDigits = trimmed.length
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -45,19 +57,30 @@ class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : Visual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PhoneOffsetMapper(val mask: String, val numberChar: Char) : OffsetMapping {
|
private class PhoneOffsetMapper(
|
||||||
|
val mask: String,
|
||||||
|
val numberChar: Char,
|
||||||
|
private val transformedLength: Int,
|
||||||
|
private val maxDigits: Int
|
||||||
|
) : OffsetMapping {
|
||||||
|
|
||||||
override fun originalToTransformed(offset: Int): Int {
|
override fun originalToTransformed(offset: Int): Int {
|
||||||
var noneDigitCount = 0
|
if (offset <= 0) return 0
|
||||||
var i = 0
|
var digitsSeen = 0
|
||||||
while (i < offset + noneDigitCount) {
|
var index = 0
|
||||||
if (mask[i++] != numberChar) noneDigitCount++
|
val targetDigits = offset.coerceAtMost(maxDigits)
|
||||||
|
|
||||||
|
while (index < mask.length && digitsSeen < targetDigits) {
|
||||||
|
if (mask[index] == numberChar) {
|
||||||
|
digitsSeen++
|
||||||
}
|
}
|
||||||
return offset + noneDigitCount
|
index++
|
||||||
|
}
|
||||||
|
return index.coerceAtMost(transformedLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transformedToOriginal(offset: Int): Int =
|
override fun transformedToOriginal(offset: Int): Int =
|
||||||
offset - mask.take(offset).count { it != numberChar }
|
mask.take(offset.coerceAtMost(transformedLength)).count { it == numberChar }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertNumberToPattern(pattern: PhoneNumberPattern, number: String): String {
|
fun convertNumberToPattern(pattern: PhoneNumberPattern, number: String): String {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.utils.ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarVisuals
|
||||||
|
|
||||||
|
enum class SnackbarStyle {
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AppSnackbarVisuals(
|
||||||
|
override val message: String,
|
||||||
|
override val actionLabel: String? = null,
|
||||||
|
override val withDismissAction: Boolean = false,
|
||||||
|
override val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||||
|
val style: SnackbarStyle = SnackbarStyle.Error
|
||||||
|
) : SnackbarVisuals
|
||||||
|
|
||||||
|
suspend fun SnackbarHostState.showSnackbar(
|
||||||
|
message: String,
|
||||||
|
style: SnackbarStyle,
|
||||||
|
actionLabel: String? = null,
|
||||||
|
withDismissAction: Boolean = false,
|
||||||
|
duration: SnackbarDuration = SnackbarDuration.Short
|
||||||
|
) = showSnackbar(
|
||||||
|
AppSnackbarVisuals(
|
||||||
|
message = message,
|
||||||
|
actionLabel = actionLabel,
|
||||||
|
withDismissAction = withDismissAction,
|
||||||
|
duration = duration,
|
||||||
|
style = style
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="1024dp"
|
||||||
|
android:height="1024dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M435.9,733L178.4,733C150.7,733 145.8,739.9 145.8,713.2L145.7,168.1C145.7,162.1 143.7,142.8 148.5,139C153.2,138.6 160.2,138.9 165.2,138.9L717.9,139C748.3,139 739.6,146.3 739.6,179L739.9,425.6C740,437.5 727.3,435.8 727.2,425.8L727.1,150.8L157.8,150.8L157.8,387.6L157.8,721L534,721C544.7,721.1 545.9,732.9 536.4,733C527.3,733.1 518.2,733 509,733L447.9,733C448.3,737.7 447.9,746.5 447.9,751.6L447.8,843.5C447.8,848.5 451.7,875.6 437.8,863.9C434.2,860.8 435.9,817.9 435.9,811.9L435.9,733Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M305.7,247.4C296.3,247.6 243.5,248.4 238.9,246.4C233,243.9 235.6,237.2 241.5,235.6C248.5,235.5 307.8,233.1 310,239C311.6,243.4 310,246.2 305.7,247.4Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M444.3,453.6C430,455.6 435.9,420.7 435.9,409L435.9,269.7C435.9,262.2 433.5,240 438.6,235.6C448.2,234.6 447.7,240.8 447.8,248.3C448,263.8 447.9,279.4 447.9,295L447.9,426.9C447.9,434.5 450.1,449.1 444.3,453.6Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M578.5,260.8C579.7,253.2 577,239.6 584.6,235.6C596,235 592.6,252.7 592.5,260.8C637.5,267.4 636.2,323.5 621.6,311.1C616.3,307 623.4,289.8 602.8,276.8C560.7,249.5 518.5,334.1 591.6,338C633.7,340.2 650.7,415 591,429C590.6,436.5 592.9,448.2 588.8,453.6C575,458.3 578.4,437.1 578.5,429C524.8,412.5 542.7,362.7 552.2,382.3C561.9,439.1 621.9,417.3 617.9,380C613.8,341.8 576.6,356.3 556.4,340.4C526.7,317 539.6,264.8 578.5,260.8Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M366.7,327L288,327C272.9,327 257.8,327.1 242.7,326.8C233.5,326.6 233.6,316.8 241.5,315.1L328.5,315.1C341.4,315.1 354.9,314.5 367.6,315.6C372.3,316 373.9,324.3 366.7,327Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M368.5,402.8C365.7,403.2 357.9,402.9 354.5,402.9L274,402.9C266.5,402.9 223.6,408.2 239,391C241.5,390.5 249.4,390.9 252.4,390.9L362.1,390.9C370.6,391.7 377.1,396.6 368.5,402.8Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M735.8,639.9C690.6,642 652.2,607.1 650,561.9C647.8,516.7 682.5,478.3 727.7,475.9C773.1,473.6 811.7,508.5 814,553.9C816.2,599.3 781.1,637.8 735.8,639.9ZM722.8,488.8C684.8,493.8 657.9,528.6 662.7,566.7C667.5,604.7 702.2,631.8 740.3,627.2C778.7,622.6 806,587.6 801.1,549.2C796.3,510.9 761.2,483.8 722.8,488.8Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M417.5,543.9C410.6,544.3 400.4,543.9 393.3,543.9L278.2,543.8C274.5,543.8 214.4,548.1 237.8,531.6C241.2,531.2 250.6,531.6 254.6,531.6L408.4,531.7C411.5,531.9 415.6,531.6 418.6,532.5C424,534.3 421.6,542 417.5,543.9Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M490.7,622.9C483.2,623.3 472.8,622.9 465.2,622.9L298.3,622.9C289.2,622.9 241.4,624 236.6,622.2C231.2,620.2 232.8,612.3 237.8,610.7L468.1,610.7C474.9,610.7 483.9,610.5 490.8,611.3C497.2,612.8 495.8,620.4 490.7,622.9Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M846.8,865L618.9,865C582.7,865 586.5,871.1 586.5,835.6L586.5,735.1C586.5,705.6 584.3,678.1 619.9,671.4C620.4,671.4 620.8,671.4 621.3,671.4L824.8,671.4C866.1,671.4 879.5,678.8 879.5,722.6L879.5,849.1C879.5,871.9 870.2,865.1 846.8,865ZM624.3,683.5C598,686.4 599,702.8 599,723.4L599,852.7L649.6,852.7L649.6,797.6C649.6,791.9 647.3,769.9 651.8,766.3C669.7,752 661.9,844.6 661.9,852.7L804.5,852.7L804.5,817.6C804.5,803 804,786.4 805.1,771.8C805.5,765.5 816.5,766.1 816.8,774C817,782.9 816.9,791.7 816.9,800.6L816.9,852.7L867.5,852.7L867.5,723.8C867.5,686 858.6,683.4 823.1,683.5L624.3,683.5Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M8.512,4.43C8.663,4.301 8.859,4.237 9.058,4.252C9.256,4.268 9.44,4.361 9.569,4.512L15.569,11.512C15.686,11.648 15.75,11.821 15.75,12C15.75,12.179 15.686,12.352 15.569,12.488L9.569,19.488C9.438,19.632 9.255,19.718 9.061,19.73C8.867,19.742 8.676,19.677 8.528,19.551C8.38,19.424 8.287,19.245 8.269,19.051C8.251,18.857 8.309,18.664 8.431,18.512L14.012,12L8.431,5.488C8.302,5.337 8.237,5.141 8.252,4.943C8.267,4.745 8.36,4.561 8.511,4.431"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MoscowHackatonTemplate</string>
|
<string name="app_name">Rekomenci fluon</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user