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
|
||||
# hide the original source file name.
|
||||
#-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,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "bf664fe902e116c42af432814d63d6a7",
|
||||
"identityHash": "3e896e9a3d3b2f61149f8c0fde7e5964",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "users",
|
||||
@@ -52,11 +52,69 @@
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "resumes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `experience_type` TEXT NOT NULL, `about_me` TEXT NOT NULL, `key_skills` TEXT NOT NULL, `position` TEXT NOT NULL, `from_salary` INTEGER, `to_salary` INTEGER, `recommended_skills` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "experienceType",
|
||||
"columnName": "experience_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "aboutMe",
|
||||
"columnName": "about_me",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keySkills",
|
||||
"columnName": "key_skills",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fromSalary",
|
||||
"columnName": "from_salary",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "toSalary",
|
||||
"columnName": "to_salary",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "recommendedSkills",
|
||||
"columnName": "recommended_skills",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf664fe902e116c42af432814d63d6a7')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e896e9a3d3b2f61149f8c0fde7e5964')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,8 @@ class App : Application() {
|
||||
androidContext(this@App)
|
||||
analytics()
|
||||
modules(
|
||||
listOf(
|
||||
AppModules().module
|
||||
)
|
||||
)
|
||||
}
|
||||
FirebaseApp.initializeApp(this@App)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.prodhack.moscow2025.common
|
||||
|
||||
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
|
||||
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider
|
||||
import org.koin.core.annotation.Configuration
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.prodhack.moscow2025.common.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
|
||||
import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@Module
|
||||
@ComponentScan("com.prodhack.moscow2025.presentation")
|
||||
@@ -13,4 +17,13 @@ class DomainModule
|
||||
|
||||
@Module
|
||||
@ComponentScan("com.prodhack.moscow2025.data")
|
||||
class DataModule
|
||||
class DataModule{
|
||||
@Single
|
||||
fun provideDatabase(context: Context): AppDatabase =
|
||||
Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
"t_tasks.db"
|
||||
).fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class BaseRemoteMediator<DBEntity : BaseEntity>(
|
||||
class BaseRemoteMediator<DBEntity : Any>(
|
||||
private val db: RoomDatabase,
|
||||
private val dao: BasePaginationDAO<DBEntity>,
|
||||
private val makeRequest: suspend (page: Long, pageCount: Int) -> Result<List<DBEntity>>
|
||||
@@ -26,17 +26,12 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
|
||||
)
|
||||
|
||||
LoadType.APPEND -> {
|
||||
val lastItem = state.lastItemOrNull()
|
||||
if (lastItem == null) {
|
||||
1
|
||||
} else {
|
||||
(lastItem.id.toLong() / state.config.pageSize) + 1
|
||||
}
|
||||
state.pages.size + 1
|
||||
}
|
||||
}
|
||||
|
||||
val result = makeRequest(
|
||||
loadKey,
|
||||
(loadKey.toLong() - 1) * state.config.pageSize,
|
||||
state.config.pageSize
|
||||
)
|
||||
|
||||
@@ -46,8 +41,7 @@ class BaseRemoteMediator<DBEntity : BaseEntity>(
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
dao.clearAll()
|
||||
}
|
||||
val beerEntities = data
|
||||
dao.upsertAll(beerEntities)
|
||||
dao.upsertAll(data)
|
||||
}
|
||||
MediatorResult.Success(
|
||||
endOfPaginationReached = data.size < state.config.pageSize
|
||||
|
||||
@@ -141,7 +141,7 @@ abstract class BaseRepository {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
protected fun <Value : BaseEntity> paginatedRequest(
|
||||
protected fun <Value : Any> paginatedRequest(
|
||||
pageSize: Int = 10,
|
||||
prefetchDistance: Int = pageSize,
|
||||
enablePlaceholders: Boolean = true,
|
||||
@@ -149,7 +149,7 @@ abstract class BaseRepository {
|
||||
maxSize: Int = Int.MAX_VALUE,
|
||||
jumpThreshold: Int = Int.MIN_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>> {
|
||||
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.RoomDatabase
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.CleanUpDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.ResumeDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
||||
|
||||
@Database(
|
||||
entities = [UserEntity::class],
|
||||
entities = [UserEntity::class, ResumeEntity::class],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
@@ -15,4 +17,5 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UserDao
|
||||
|
||||
abstract fun cleanUpDao(): CleanUpDao
|
||||
abstract fun resumeDao(): ResumeDao
|
||||
}
|
||||
|
||||
+1
-8
@@ -8,12 +8,5 @@ import org.koin.core.annotation.Single
|
||||
@Module
|
||||
class DatabaseProvider {
|
||||
|
||||
@Single
|
||||
fun provideDatabase(context: Context): AppDatabase =
|
||||
Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
"t_tasks.db"
|
||||
).fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
+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.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ErrorNetworkDTO(
|
||||
val detail: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserPatchRequest(
|
||||
val email: String?,
|
||||
@SerialName("display_name")
|
||||
val displayName: String? = null,
|
||||
@SerialName("first_name")
|
||||
val firstName: String? = null,
|
||||
@SerialName("last_name")
|
||||
val lastName: String? = null,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String? = null,
|
||||
val phone: String? = null,
|
||||
)
|
||||
|
||||
fun UpdateUserData.mapToData(): UserPatchRequest = UserPatchRequest(
|
||||
email = email,
|
||||
displayName = displayName,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
avatarUrl = avatarUrl,
|
||||
phone = phone
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserLoginRequest(
|
||||
@@ -58,28 +31,3 @@ data class TokenResponse(
|
||||
@SerialName("access_token")
|
||||
val token: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserResponse(
|
||||
val id: String,
|
||||
val email: String,
|
||||
@SerialName("display_name")
|
||||
val displayName: String? = null,
|
||||
@SerialName("first_name")
|
||||
val firstName: String? = null,
|
||||
@SerialName("last_name")
|
||||
val lastName: String? = null,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String? = null,
|
||||
val phone: String? = null,
|
||||
) {
|
||||
fun mapToDomain(): User = User(
|
||||
id = id,
|
||||
email = email,
|
||||
displayName = displayName,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
avatarUrl = avatarUrl,
|
||||
phone = phone
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import android.util.Log
|
||||
import com.prodhack.moscow2025.domain.interfaces.AuthRepository
|
||||
import com.prodhack.moscow2025.domain.interfaces.UserRepository
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@@ -16,14 +17,22 @@ class CheckSessionUseCase(
|
||||
private val authRepository: AuthRepository,
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
const val TAG = "CheckSessionUseCase"
|
||||
}
|
||||
|
||||
/**
|
||||
* return session state with
|
||||
* @return session state in enum format [SessionState]
|
||||
*/
|
||||
suspend operator fun invoke(): SessionState =
|
||||
if (authRepository.fetchLoginState().firstOrNull() == true) {
|
||||
Log.d(TAG, "user authorized, requesting profile")
|
||||
if (userRepository.fetchProfile().getOrNull()?.firstName.isNullOrBlank()) {
|
||||
Log.d(TAG, "user authorized, first name is blank -> need fill profile")
|
||||
SessionState.NotFilledProfile
|
||||
} else {
|
||||
Log.d(TAG, "user authorized, first name is filled -> user already fill profile")
|
||||
SessionState.FilledAndAuthorized
|
||||
}
|
||||
} else {
|
||||
|
||||
+19
@@ -24,6 +24,25 @@ data class ValidationResult(
|
||||
|
||||
@Single
|
||||
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(
|
||||
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() {
|
||||
|
||||
private companion object {
|
||||
const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
private val checkSessionUseCase: CheckSessionUseCase by inject()
|
||||
|
||||
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
|
||||
@@ -42,8 +46,11 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
runBlocking {
|
||||
val sessionState = try {
|
||||
checkSessionUseCase()
|
||||
checkSessionUseCase().also {
|
||||
Log.d(TAG, "SessionState received $it")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception in session state getting process", e)
|
||||
SessionState.NotAuthorized
|
||||
}
|
||||
sessionDestinationState.value =
|
||||
@@ -67,7 +74,6 @@ class MainActivity : ComponentActivity() {
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
val token = task.result
|
||||
Log.d("TOKEN", token)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -4,6 +4,8 @@ import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -59,7 +61,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
||||
}
|
||||
target?.let { (it - center).toDp() }
|
||||
}
|
||||
AnimatedVisibility(indicatorOffset != null) {
|
||||
indicatorOffset?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -71,7 +72,6 @@ fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Icon(
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -28,17 +29,17 @@ fun TopLogo(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
Icon(
|
||||
modifier = Modifier.size(100.dp),
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
painter = painterResource(R.drawable.app_logo),
|
||||
contentDescription = "App logo"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(Paddings.medium))
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = 48.sp
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 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 com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
||||
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.AppSnackbarVisuals
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||
|
||||
@Composable
|
||||
fun TTasksApp(
|
||||
@@ -26,7 +28,7 @@ fun TTasksApp(
|
||||
context: Context,
|
||||
sessionDestination: AppDestination? = null
|
||||
) {
|
||||
MoscowHackatonTemplateTheme() {
|
||||
MoscowHackatonTemplateTheme {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val bottomBarState = remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
@@ -53,10 +55,19 @@ fun TTasksApp(
|
||||
SnackbarHost(
|
||||
hostState = snackbarHostState,
|
||||
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(
|
||||
snackbarData = data,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.prodhack.moscow2025.presentation.navigation
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
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.profile.ProfileScreen
|
||||
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.ErrorCollectorScope
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -88,12 +91,25 @@ fun TTasksNavHost(
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.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
|
||||
@@ -144,11 +145,10 @@ fun ErrorCollectorScope.FillProfileScreen(
|
||||
style = typography.titleLarge,
|
||||
fontSize = 31.sp
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.app_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(140.dp),
|
||||
contentScale = ContentScale.Crop
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
@@ -166,70 +166,16 @@ fun ErrorCollectorScope.FillProfileScreen(
|
||||
error = formState.errors[AuthField.LastName],
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
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 = viewModel.chosenPattern.value?.prefix ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
textStyle = TextStyle(
|
||||
color = colorScheme.onPrimary
|
||||
),
|
||||
decorationBox = { innerTextField ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
TPhoneField(
|
||||
currentPattern = viewModel.currentPattern.value,
|
||||
currentPhone = formState.phone,
|
||||
onPhoneChange = viewModel::onPhoneChange,
|
||||
error = formState.errors[AuthField.Phone],
|
||||
onOpenCountryList =
|
||||
{
|
||||
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))
|
||||
BigButton(
|
||||
@@ -242,32 +188,12 @@ fun ErrorCollectorScope.FillProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (isSheetOpen.value) {
|
||||
ModalBottomSheet(
|
||||
TPhoneCountryList(
|
||||
isSheetOpen = isSheetOpen,
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
isSheetOpen.value = false
|
||||
},
|
||||
) {
|
||||
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
|
||||
patternList = viewModel.phoneNumberPatterns,
|
||||
setPattern = {
|
||||
viewModel.currentPattern.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
data class FillProfileFormState(
|
||||
val firstName: String = "",
|
||||
@@ -95,9 +94,11 @@ class FillProfileViewModel(
|
||||
}
|
||||
|
||||
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 {
|
||||
it.copy(
|
||||
phone = value,
|
||||
phone = digits,
|
||||
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>()
|
||||
|
||||
|
||||
fun update() {
|
||||
// Load default pattern
|
||||
chosenPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||
currentPattern.value = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||
|
||||
// Load all phone number patterns
|
||||
phoneNumberPatterns.clear()
|
||||
@@ -171,7 +172,7 @@ class FillProfileViewModel(
|
||||
firstName = _formStateFillProfile.value.firstName,
|
||||
lastName = _formStateFillProfile.value.lastName,
|
||||
phone = _formStateFillProfile.value.phone,
|
||||
chosenPattern = chosenPattern.value?.mapToDomain()
|
||||
chosenPattern = currentPattern.value?.mapToDomain()
|
||||
)
|
||||
|
||||
if (!validation.isValid) {
|
||||
@@ -185,7 +186,7 @@ class FillProfileViewModel(
|
||||
UpdateUserData(
|
||||
firstName = _formStateFillProfile.value.firstName,
|
||||
lastName = _formStateFillProfile.value.lastName,
|
||||
phone = chosenPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
||||
phone = currentPattern.value?.mapToDomain()?.let { phoneNumberPattern ->
|
||||
convertNumberToPattern(
|
||||
phoneNumberPattern,
|
||||
_formStateFillProfile.value.phone
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -127,8 +128,8 @@ fun ErrorCollectorScope.LoginScreen(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.app_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
|
||||
+162
-263
@@ -1,9 +1,39 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.main
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.prodhack.moscow2025.R
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTFloatingActionButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TopLogo
|
||||
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
|
||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@@ -12,271 +42,140 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@Composable
|
||||
fun ErrorCollectorScope.MainScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
openResumeDetails: (String) -> Unit,
|
||||
viewModel: MainScreenViewModel = koinViewModel()
|
||||
) {
|
||||
Text("Main screen will be here soon")
|
||||
// val openCalendarModal = remember { mutableStateOf(false) }
|
||||
// val openTaskAddSheet = remember { mutableStateOf(false) }
|
||||
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
// val tasks = viewModel.taskList.collectAsLazyPagingItems()
|
||||
//
|
||||
// val selectedTask = remember { mutableStateOf<UITaskModel?>(null) }
|
||||
//
|
||||
// Box(
|
||||
// modifier = modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(horizontal = Paddings.large),
|
||||
// contentAlignment = Alignment.BottomCenter
|
||||
// ) {
|
||||
// Column(
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
// ) {
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
// TopLogo()
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
//
|
||||
// MainScreenFilters(viewModel = viewModel) {
|
||||
// openCalendarModal.value = true
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
//
|
||||
// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics ->
|
||||
// BubbledCategoryFilters(
|
||||
// categories = topics,
|
||||
// selectedItemId = viewModel.selectedTopicId.value ?: -1
|
||||
// ) { categoryId ->
|
||||
// viewModel.selectTopic(categoryId)
|
||||
// }
|
||||
// }
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
//
|
||||
// if (tasks.loadState.hasError) {
|
||||
// Text(
|
||||
// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!",
|
||||
// style = Typography.titleMedium,
|
||||
// textAlign = TextAlign.Center,
|
||||
// fontSize = 18.sp,
|
||||
// color = MaterialTheme.colorScheme.error
|
||||
// )
|
||||
// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) {
|
||||
// Spacer(modifier = Modifier.weight(1f))
|
||||
//
|
||||
// Text(
|
||||
// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку",
|
||||
// style = Typography.titleMedium,
|
||||
// textAlign = TextAlign.Center,
|
||||
// fontSize = 18.sp,
|
||||
// color = MaterialTheme.colorScheme.onBackground
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
// BigButton(buttonText = "Начать", onClick = {
|
||||
//
|
||||
// }, isLoading = false)
|
||||
//
|
||||
// Spacer(modifier = Modifier.weight(3f))
|
||||
//
|
||||
// } else {
|
||||
// LazyColumn(
|
||||
// verticalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
// ) {
|
||||
// items(tasks.itemCount) { it ->
|
||||
// val task = tasks[it]
|
||||
// task?.let {
|
||||
// TaskCard(
|
||||
// onClick = {
|
||||
// selectedTask.value = it
|
||||
// },
|
||||
// taskInfo = it,
|
||||
// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value
|
||||
// ) {
|
||||
// viewModel.toggleTaskAsDone(
|
||||
// tripId = it.tripId,
|
||||
// taskId = it.id,
|
||||
// currState = it.archived
|
||||
// )
|
||||
// tasks.refresh()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// item {
|
||||
// if (!tasks.loadState.append.endOfPaginationReached) {
|
||||
// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// TTFloatingActionButton(
|
||||
// modifier = Modifier
|
||||
// .align(Alignment.BottomCenter)
|
||||
// .padding(bottom = Paddings.medium),
|
||||
// onClick = {
|
||||
// openTaskAddSheet.value = true
|
||||
// },
|
||||
// text = "Добавить задачу"
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//
|
||||
// AnimatedVisibility(openCalendarModal.value) {
|
||||
// DateRangePickerModal({
|
||||
// Log.d("DatePicker", it.toString())
|
||||
// if (it.first != null && it.second != null) {
|
||||
// viewModel.setDate(Pair(it.first!!, it.second!!))
|
||||
// openCalendarModal.value = false
|
||||
// }
|
||||
// }) {
|
||||
// openCalendarModal.value = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (openTaskAddSheet.value) {
|
||||
// AddTaskBottomSheet(
|
||||
// sheetState = sheetState,
|
||||
// onDismiss = {
|
||||
// openTaskAddSheet.value = false
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// val cs = MaterialTheme.colorScheme
|
||||
//
|
||||
// val viewSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
//
|
||||
// if (selectedTask.value != null) {
|
||||
//
|
||||
// val openCalendarModal2 = remember { mutableStateOf(false) }
|
||||
//
|
||||
// ModalBottomSheet(
|
||||
// onDismissRequest = {
|
||||
// selectedTask.value = null
|
||||
// },
|
||||
// sheetState = viewSheetState,
|
||||
// dragHandle = {},
|
||||
// shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
|
||||
// ) {
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
// .verticalScroll(rememberScrollState()),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "Просмотр задачи",
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.titleMedium,
|
||||
// fontSize = 22.sp,
|
||||
// textAlign = TextAlign.Center,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
//
|
||||
// Text(
|
||||
// text = selectedTask.value!!.name,
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.titleMedium,
|
||||
// fontSize = 20.sp,
|
||||
// textAlign = TextAlign.Center,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
//
|
||||
//
|
||||
// Text(
|
||||
// text = "Что нужно сделать",
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.titleMedium,
|
||||
// fontSize = 18.sp,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.small))
|
||||
//
|
||||
// Text(
|
||||
// text = selectedTask.value!!.whatNeedToDo,
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.labelLarge,
|
||||
// fontSize = 16.sp,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
//
|
||||
// Text(
|
||||
// text = "Для чего",
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.titleMedium,
|
||||
// fontSize = 18.sp,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.small))
|
||||
//
|
||||
// Text(
|
||||
// text = selectedTask.value!!.reason,
|
||||
// color = cs.onSurface,
|
||||
// style = Typography.labelLarge,
|
||||
// fontSize = 16.sp,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(bottom = 24.dp, top = 8.dp)
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(Paddings.large))
|
||||
//
|
||||
// TTTextField(
|
||||
// onClick = {
|
||||
// openCalendarModal2.value = true
|
||||
// },
|
||||
// value = timestampToDateWithYear(selectedTask.value!!.deadline),
|
||||
// readOnly = true,
|
||||
// onValueChange = {},
|
||||
// label = "Дедлайн",
|
||||
// trailingIcon = {
|
||||
// Icon(
|
||||
// modifier = Modifier
|
||||
// .size(24.dp),
|
||||
// painter = painterResource(
|
||||
// R.drawable.ic_calendar
|
||||
// ),
|
||||
// tint = MaterialTheme.colorScheme.onPrimary,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// AnimatedVisibility(openCalendarModal2.value) {
|
||||
// DatePickerModal({
|
||||
// Log.d("DatePicker", it.toString())
|
||||
// it?.let { date ->
|
||||
// viewModel.changeTaskDeadline(selectedTask.value, date)
|
||||
// selectedTask.value = null
|
||||
// openCalendarModal.value = false
|
||||
// }
|
||||
// }) {
|
||||
// openCalendarModal.value = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
val typography = MaterialTheme.typography
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val shapes = MaterialTheme.shapes
|
||||
|
||||
Box {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TopLogo()
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
Text(
|
||||
text = "Ваши резюме",
|
||||
style = typography.titleLarge,
|
||||
fontSize = 32.sp,
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
val items = viewModel.resumeList.collectAsLazyPagingItems()
|
||||
|
||||
if (items.itemCount == 0 && items.loadState.append.endOfPaginationReached) {
|
||||
Text(
|
||||
text = "Здесь пока ничего нет",
|
||||
style = typography.labelLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 24.sp,
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
|
||||
BigButton(onClick = {
|
||||
TODO()
|
||||
}, buttonText = "Создать резюме", isLoading = false)
|
||||
} else if (items.loadState.hasError) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(colorScheme.error, shape = shapes.small)
|
||||
.padding(Paddings.medium),
|
||||
text = "Кажется что-то пошло не так, но мы уже чиним 🛠️",
|
||||
style = typography.labelLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 24.sp,
|
||||
color = colorScheme.onError
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
Paddings.medium
|
||||
)
|
||||
) {
|
||||
items(items.itemCount) {
|
||||
items[it]?.let { resume ->
|
||||
ResumeShortInfoCard(info = resume) {
|
||||
openResumeDetails(resume.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (items.loadState.append.endOfPaginationReached.not()) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
TTFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = Paddings.medium),
|
||||
onClick = {
|
||||
Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
text = "Добавить резюме"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResumeShortInfoCard(
|
||||
modifier: Modifier = Modifier,
|
||||
info: UIResumeBaseInfo,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val typography = MaterialTheme.typography
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Paddings.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||
) {
|
||||
Text(info.positionName, style = typography.labelLarge, fontSize = 20.sp)
|
||||
Row {
|
||||
Text(
|
||||
"Ожидаемая ЗП: ",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Text(
|
||||
"${info.salary}₽",
|
||||
style = typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_arr_details),
|
||||
contentDescription = "Open details"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-133
@@ -1,143 +1,17 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.main
|
||||
|
||||
import androidx.paging.map
|
||||
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase
|
||||
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
|
||||
import com.prodhack.moscow2025.presentation.dataModels.mapToBaseUIInfo
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
|
||||
|
||||
@KoinViewModel
|
||||
class MainScreenViewModel(
|
||||
// private val loadTasksUseCase: LoadTasksUseCase,
|
||||
// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase,
|
||||
// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase,
|
||||
// private val changeDeadlineUseCase: ChangeDeadlineUseCase
|
||||
loadResumeListUseCase: LoadResumeListUseCase
|
||||
) : BaseViewModel() {
|
||||
|
||||
// var userChanged = false
|
||||
//
|
||||
// // Date filter
|
||||
// private val defaultDateFilterState =
|
||||
// getStartOfTodayTimestamp().let { Pair(it, it + 86400000) }
|
||||
//
|
||||
//
|
||||
// private val dateState =
|
||||
// mutableStateOf(defaultDateFilterState)
|
||||
//
|
||||
// val dateString = derivedStateOf {
|
||||
// Log.d(
|
||||
// "MainScreenViewModel",
|
||||
// "deriving state <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()
|
||||
// }
|
||||
val resumeList = loadResumeListUseCase().map { it -> it.map { it.mapToBaseUIInfo() } }
|
||||
}
|
||||
|
||||
+231
-1
@@ -1,9 +1,239 @@
|
||||
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.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.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
|
||||
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.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -111,8 +112,8 @@ fun ErrorCollectorScope.RegisterScreen(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.app_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
|
||||
+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 androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
@@ -149,6 +150,10 @@ fun MoscowHackatonTemplateTheme(
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
shapes = Shapes(
|
||||
extraSmall = com.prodhack.moscow2025.presentation.theme.Shapes.verySmallRoundedBox,
|
||||
small = com.prodhack.moscow2025.presentation.theme.Shapes.smallRoundedBox
|
||||
),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
+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 {
|
||||
@@ -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 {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (mask[i++] != numberChar) noneDigitCount++
|
||||
if (offset <= 0) return 0
|
||||
var digitsSeen = 0
|
||||
var index = 0
|
||||
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 =
|
||||
offset - mask.take(offset).count { it != numberChar }
|
||||
mask.take(offset.coerceAtMost(transformedLength)).count { it == numberChar }
|
||||
}
|
||||
|
||||
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>
|
||||
<string name="app_name">MoscowHackatonTemplate</string>
|
||||
<string name="app_name">Rekomenci fluon</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user