From 3ebb53e3cc0c7ba9f690a033aafc63f3176df402 Mon Sep 17 00:00:00 2001 From: norhanadel Date: Sat, 18 Oct 2025 09:34:05 +0300 Subject: [PATCH 1/5] create location screen --- .../kotlin/org/example/project/MyApp.kt | 2 +- .../composeResources/values-ar/string.xml | 13 + .../composeResources/values/string.xml | 13 + .../kotlin/org/example/project/App.kt | 3 +- .../example/project/data/dto/DistrictDto.kt | 10 + .../project/data/dto/GovernoratesDto.kt | 10 + .../data/repository/LocationRepositoryImpl.kt | 32 ++ .../data/repository/mapper/District.kt | 13 + .../data/repository/mapper/Gavernorate.kt | 13 + .../project/data/utils/NetworkConstants.kt | 4 + .../org/example/project/di/CraftoModule.kt | 12 +- .../org/example/project/di/ViewModelModule.kt | 11 + .../kotlin/org/example/project/di/initKoin.kt | 10 +- .../example/project/domain/entity/District.kt | 6 + .../project/domain/entity/Governorates.kt | 6 + .../domain/repository/LocationRepository.kt | 9 + .../location/AccountSetupTopBar.kt | 84 +++++ .../setupScreens/location/LocationEffect.kt | 6 + .../location/LocationSetupScreen.kt | 304 ++++++++++++++++++ .../setupScreens/location/LocationUiState.kt | 16 + .../location/LocationViewModel.kt | 93 ++++++ .../org/example/project/MainViewController.kt | 2 +- 22 files changed, 661 insertions(+), 11 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/dto/GovernoratesDto.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Gavernorate.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Governorates.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/repository/LocationRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt b/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt index de685ea..5d969e4 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt @@ -1,7 +1,7 @@ package org.example.project import android.app.Application -import org.example.project.di.initKoin +import initKoin class MyApp : Application() { override fun onCreate() { diff --git a/composeApp/src/commonMain/composeResources/values-ar/string.xml b/composeApp/src/commonMain/composeResources/values-ar/string.xml index 4659eed..66e457e 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/string.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/string.xml @@ -24,4 +24,17 @@ التالى تخطى + + + أين موقعك؟ + الموقع يساعد في تحسين الدقة، لكن لا تقلق، يمكنك تحديثه لاحقًا. + المحافظة، المنطقة + اختر المحافظة + اختر المنطقة + التالي + أيقونة الموقع + أيقونة القائمة المنسدلة + أيقونة السهم + أدخل موقعك بالتفصيل + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/string.xml b/composeApp/src/commonMain/composeResources/values/string.xml index afd1afe..e60ad4c 100644 --- a/composeApp/src/commonMain/composeResources/values/string.xml +++ b/composeApp/src/commonMain/composeResources/values/string.xml @@ -33,4 +33,17 @@ What services do you offer? Choose your specialties to get relevant job requests. You can change this later. + + + Where are you located? + Location helps improve accuracy, but don\'t worry, you can update it later. + Governorate, District + Choose Governorate + Choose District + Next + Location icon + Dropdown + Arrow + Enter your location in detail + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index 1236764..a819006 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -3,12 +3,13 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.onboarding.OnboardingScreen +import org.example.project.presentation.screens.setupScreens.location.LocationSetupScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - OnboardingScreen() + LocationSetupScreen() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt new file mode 100644 index 0000000..6ed53e3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt @@ -0,0 +1,10 @@ +package org.example.project.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DistrictDto( + @SerialName("id") val id: String, + @SerialName("name") val name: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/GovernoratesDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/GovernoratesDto.kt new file mode 100644 index 0000000..1fad12c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/GovernoratesDto.kt @@ -0,0 +1,10 @@ +package org.example.project.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GovernoratesDto( + @SerialName("id") val id: String, + @SerialName("name") val name: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt new file mode 100644 index 0000000..360ac85 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt @@ -0,0 +1,32 @@ +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.call.body +import org.example.project.data.dto.DistrictDto +import org.example.project.data.dto.GovernoratesDto +import org.example.project.data.repository.mapper.toDistrict +import org.example.project.data.repository.mapper.toGovernorates +import org.example.project.data.utils.NetworkConstants.DISTRICT_ENDPOINT +import org.example.project.data.utils.NetworkConstants.GOVERNORATES_ENDPOINT +import org.example.project.data.utils.NetworkConstants.LOCATION_PATH +import org.example.project.domain.entity.District +import org.example.project.domain.entity.Governorates +import org.example.project.domain.repository.LocationRepository +import org.koin.core.annotation.Provided +import org.koin.core.annotation.Single + +internal class LocationRepositoryImpl( + private val httpClient: HttpClient +) : LocationRepository { + + override suspend fun getAllGovernorates(): List { + val response = httpClient.get("/$LOCATION_PATH/$GOVERNORATES_ENDPOINT") + val body: List = response.body() + return body.toGovernorates() + } + + override suspend fun getDistrictsByGovernorateId(governorateId: String): List { + val response = httpClient.get("/$LOCATION_PATH/$DISTRICT_ENDPOINT/$governorateId") + val body: List = response.body() + return body.toDistrict() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt new file mode 100644 index 0000000..628892e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt @@ -0,0 +1,13 @@ +package org.example.project.data.repository.mapper + +import org.example.project.data.dto.DistrictDto +import org.example.project.domain.entity.District + +fun List.toDistrict(): List { + return map { dto -> + District( + id = dto.id, + name = dto.name + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Gavernorate.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Gavernorate.kt new file mode 100644 index 0000000..f7b5233 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Gavernorate.kt @@ -0,0 +1,13 @@ +package org.example.project.data.repository.mapper + +import org.example.project.data.dto.GovernoratesDto +import org.example.project.domain.entity.Governorates + +fun List.toGovernorates(): List { + return map { dto -> + Governorates( + id = dto.id, + name = dto.name + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt index 9c7d8c5..8e09a6f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt @@ -2,4 +2,8 @@ package org.example.project.data.utils object NetworkConstants { const val ONBOARDING_END_POINT = "/onboarding" + + const val LOCATION_PATH = "location" + const val GOVERNORATES_ENDPOINT = "governorates" + const val DISTRICT_ENDPOINT = "district" } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt index 032127c..b7aca13 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt @@ -1,8 +1,10 @@ -package org.example.project.di - -import org.koin.core.annotation.ComponentScan +import org.example.project.domain.repository.LocationRepository import org.koin.core.annotation.Module +import org.koin.dsl.module @Module -@ComponentScan("org.example.project") -class CraftoModule \ No newline at end of file +class CraftoModule { + val module = module { + single { LocationRepositoryImpl(get()) } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt new file mode 100644 index 0000000..25a13a5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt @@ -0,0 +1,11 @@ +package org.example.project.di + +import org.example.project.presentation.screens.setupScreens.location.LocationViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +class ViewModelModule { + val module = module { + viewModel { LocationViewModel(get()) } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt index a4aba07..c6b2ef1 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -1,5 +1,5 @@ -package org.example.project.di - +import org.example.project.di.NetworkModule +import org.example.project.di.ViewModelModule import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration import org.koin.ksp.generated.module @@ -7,6 +7,10 @@ import org.koin.ksp.generated.module fun initKoin(config: KoinAppDeclaration? = null) { startKoin { config?.invoke(this) - modules(CraftoModule().module, NetworkModule().module) + modules( + CraftoModule().module, + ViewModelModule().module, + NetworkModule().module + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt new file mode 100644 index 0000000..792f3d9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt @@ -0,0 +1,6 @@ +package org.example.project.domain.entity + +data class District( + val id: String, + val name: String, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Governorates.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Governorates.kt new file mode 100644 index 0000000..eabb1af --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Governorates.kt @@ -0,0 +1,6 @@ +package org.example.project.domain.entity + +data class Governorates( + val id: String, + val name: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/LocationRepository.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/LocationRepository.kt new file mode 100644 index 0000000..f352d76 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/LocationRepository.kt @@ -0,0 +1,9 @@ +package org.example.project.domain.repository + +import org.example.project.domain.entity.District +import org.example.project.domain.entity.Governorates + +interface LocationRepository { + suspend fun getAllGovernorates(): List + suspend fun getDistrictsByGovernorateId(governorateId: String): List +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt new file mode 100644 index 0000000..28fd946 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt @@ -0,0 +1,84 @@ +package org.example.project.presentation.screens.setupScreens.location + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.arrow_left +import org.example.project.presentation.designsystem.components.ProgressIndicator +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun AccountSetupTopBar( + modifier: Modifier = Modifier, + currentPage: Int +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) + { + BackButton() + ProgressIndicator( + currentPage = currentPage, + totalPage = 4, + modifier = Modifier.fillMaxWidth(0.75f), + ) + } +} + +@Composable +private fun BackButton( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(48.dp) + .clip(CircleShape) + .background(AppTheme.craftoColors.background.card), + contentAlignment = Alignment.Center + ) + { + Icon( + painter = painterResource(Res.drawable.arrow_left), + contentDescription = "back button", + tint = AppTheme.craftoColors.shade.primary + ) + } +} + + +@Preview +@Composable +fun AccountSetupTopBarLightPreview() { + AppTheme { + AccountSetupTopBar( + currentPage = 2 + ) + + } +} + +@Preview +@Composable +fun AccountSetupTopBarDarkPreview() { + AppTheme(isDarkTheme = true) { + AccountSetupTopBar( + currentPage = 2 + + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt new file mode 100644 index 0000000..2d5e88b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt @@ -0,0 +1,6 @@ +package org.example.project.presentation.screens.setupScreens.location + +sealed class LocationEffect { + object NavigateToNextScreen : LocationEffect() + data class ShowError(val message: String) : LocationEffect() +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt new file mode 100644 index 0000000..24a667b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt @@ -0,0 +1,304 @@ +package org.example.project.presentation.screens.setupScreens.location + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +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.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.* +import org.example.project.domain.entity.District +import org.example.project.domain.entity.Governorates +import org.example.project.presentation.designsystem.components.BottomSheet +import org.example.project.presentation.designsystem.components.ButtonState +import org.example.project.presentation.designsystem.components.PrimaryButton +import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.annotation.KoinExperimentalAPI + +private val FieldHeight = 48.dp +private val DefaultPadding = 16.dp + +@OptIn(KoinExperimentalAPI::class) +@Composable +fun LocationSetupScreen( + viewModel: LocationViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsState() + + HandleEffects(viewModel) + + val displayText by remember(state.selectedGovernorate, state.selectedDistrict) { + derivedStateOf { + val parts = listOf(state.selectedGovernorate, state.selectedDistrict).filter { it.isNotBlank() } + if (parts.isEmpty())"Governorate, District" else parts.joinToString(", ") + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing) + .background(AppTheme.craftoColors.background.screen) + .padding(DefaultPadding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(DefaultPadding) + ) { + AccountSetupTopBar( + modifier = Modifier.fillMaxWidth(), + currentPage = 2 + ) + + LocationHeader(modifier = Modifier.weight(1f)) + + GovernorateSelector( + displayText = displayText, + hasSelection = displayText != stringResource(Res.string.location_hint), + onClick = viewModel::openGovernorateSheet + ) + + DetailLocationInput( + text = state.detailLocation, + onTextChange = viewModel::updateDetailLocation + ) + + ErrorMessage(error = state.error) + + NextButton(onClick = viewModel::onNextClick) + } + + GovernorateBottomSheet( + show = state.showGovernorateSheet, + governorates = state.governorates, + onDismiss = viewModel::closeGovernorateSheet, + onSelect = viewModel::selectGovernorate + ) + + DistrictBottomSheet( + show = state.showDistrictSheet, + districts = state.districts, + onDismiss = viewModel::closeDistrictSheet, + onSelect = viewModel::selectDistrict + ) +} + +@Composable +private fun HandleEffects(viewModel: LocationViewModel) { + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + is LocationEffect.NavigateToNextScreen -> { + // TODO + } + is LocationEffect.ShowError -> { + // TODO + } + } + } + } +} + +@Composable +private fun LocationHeader(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Bottom, + ) { + Text( + text = stringResource(Res.string.location_title), + style = AppTheme.textStyle.display, + color = AppTheme.craftoColors.shade.primary, + modifier = Modifier.padding(bottom = DefaultPadding) + ) + + Text( + text = stringResource(Res.string.location_description), + style = AppTheme.textStyle.body.largeRegular, + color = AppTheme.craftoColors.shade.secondary + ) + } +} + +@Composable +private fun GovernorateSelector( + displayText: String, + hasSelection: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(FieldHeight) + .background( + color = AppTheme.craftoColors.background.card, + shape = RoundedCornerShape(AppTheme.craftoRadius.lg) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + hint = stringResource(Res.string.location_hint), + text = displayText, + onTextChange = {}, + modifier = Modifier.fillMaxWidth(), + startIcon = { + Icon( + painter = painterResource(Res.drawable.location), + contentDescription = stringResource(Res.string.location_icon), + tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary + ) + }, + endIcon = { + Icon( + painter = painterResource(Res.drawable.alt_arrow_down), + contentDescription = stringResource(Res.string.dropdown_icon), + tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary, + modifier = Modifier.clickable(onClick = onClick) + ) + } + ) + } +} + +@Composable +private fun DetailLocationInput( + text: String, + onTextChange: (String) -> Unit +) { + TextField( + hint = stringResource(Res.string.enter_detailed_location), + text = text, + onTextChange = onTextChange, + modifier = Modifier.fillMaxWidth() + ) +} + +@Composable +private fun ErrorMessage(error: String?) { + if (error != null) { + Text( + text = error, + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.shade.quinary, + modifier = Modifier.padding(top = 8.dp) + ) + } +} + +@Composable +private fun NextButton(onClick: () -> Unit) { + PrimaryButton( + text = stringResource(Res.string.next_button), + enabled = true, + onClick = onClick, + buttonState = ButtonState.Enable, + modifier = Modifier.fillMaxWidth() + ) +} + +@Composable +private fun GovernorateBottomSheet( + show: Boolean, + governorates: List, + onDismiss: () -> Unit, + onSelect: (Governorates) -> Unit +) { + if (show) { + BottomSheet( + onDismissRequest = onDismiss + ) { + LazyColumn { + items(governorates.size) { index -> + val governorate = governorates[index] + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(governorate) } + .padding(DefaultPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = governorate.name, + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.shade.primary, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(Res.drawable.alt_arrow_down), + contentDescription = stringResource(Res.string.arrow_icon), + tint = AppTheme.craftoColors.shade.secondary + ) + } + } + } + } + } +} + +@Composable +private fun DistrictBottomSheet( + show: Boolean, + districts: List, + onDismiss: () -> Unit, + onSelect: (String) -> Unit +) { + if (show) { + BottomSheet( + onDismissRequest = onDismiss + ) { + LazyColumn { + items(districts.size) { index -> + val district = districts[index] + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(district.name) } + .padding(DefaultPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = district.name, + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.shade.primary, + modifier = Modifier.weight(1f) + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun LocationSetupScreenPreview() { + AppTheme(isDarkTheme = false) { + LocationSetupScreen() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt new file mode 100644 index 0000000..0d890c8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt @@ -0,0 +1,16 @@ +package org.example.project.presentation.screens.setupScreens.location + +import org.example.project.domain.entity.District +import org.example.project.domain.entity.Governorates + +data class LocationUiState( + val governorates: List = emptyList(), + val districts: List = emptyList(), + val selectedGovernorate: String = "", + val selectedDistrict: String = "", + val detailLocation: String = "", + val isLoading: Boolean = false, + val error: String? = null, + val showGovernorateSheet: Boolean = false, + val showDistrictSheet: Boolean = false +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt new file mode 100644 index 0000000..11860d4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt @@ -0,0 +1,93 @@ +package org.example.project.presentation.screens.setupScreens.location + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import org.example.project.domain.entity.Governorates +import org.example.project.domain.repository.LocationRepository +import org.example.project.presentation.shared.base.BaseViewModel + +class LocationViewModel( + private val repository: LocationRepository +) : BaseViewModel(initialState = LocationUiState()) { + + + init { + fetchGovernorates() + } + + private fun fetchGovernorates() { + tryToCall( + call = { + repository.getAllGovernorates() }, + onSuccess = { governorates -> + updateState { it.copy(governorates = governorates, isLoading = false, error = null) } + }, + onError = { error -> + updateState { it.copy(isLoading = false, error = error.message) } + sendNewEffect(LocationEffect.ShowError(error.message)) + }, + dispatcher = Dispatchers.IO + ) + } + + fun fetchDistricts(governorateId: String) { + tryToCall( + call = { + repository.getDistrictsByGovernorateId(governorateId) }, + onSuccess = { districts -> + updateState { + it.copy( + districts = districts, + isLoading = false, + showDistrictSheet = districts.isNotEmpty(), + error = null + ) + } + }, + onError = { error -> + updateState { it.copy(isLoading = false, error = error.message) } + sendNewEffect(LocationEffect.ShowError(error.message)) + }, + dispatcher = Dispatchers.IO + ) + } + fun selectGovernorate(governorate: Governorates) { + updateState { + it.copy( + selectedGovernorate = governorate.name, + selectedDistrict = "", + showGovernorateSheet = false + ) + } + fetchDistricts(governorate.id) + } + + fun selectDistrict(district: String) { + updateState { + it.copy( + selectedDistrict = district, + showDistrictSheet = false + ) + } + } + + fun updateDetailLocation(text: String) { + updateState { it.copy(detailLocation = text) } + } + + fun openGovernorateSheet() { + updateState { it.copy(showGovernorateSheet = true) } + } + + fun closeGovernorateSheet() { + updateState { it.copy(showGovernorateSheet = false) } + } + + fun closeDistrictSheet() { + updateState { it.copy(showDistrictSheet = false) } + } + + fun onNextClick() { + sendNewEffect(LocationEffect.NavigateToNextScreen) + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt index f581b42..a04ca7d 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt @@ -1,7 +1,7 @@ package org.example.project import androidx.compose.ui.window.ComposeUIViewController -import org.example.project.di.initKoin +import initKoin fun MainViewController() = ComposeUIViewController( configure = { From 2990d345894ccb7119e99b43c8e6e1ade500ece9 Mon Sep 17 00:00:00 2001 From: norhanadel Date: Sat, 18 Oct 2025 13:10:04 +0300 Subject: [PATCH 2/5] edit app file --- composeApp/src/commonMain/kotlin/org/example/project/App.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index a819006..1236764 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -3,13 +3,12 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.onboarding.OnboardingScreen -import org.example.project.presentation.screens.setupScreens.location.LocationSetupScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - LocationSetupScreen() + OnboardingScreen() } } \ No newline at end of file From 14e0316336392ba2eee7925d132386017586d4df Mon Sep 17 00:00:00 2001 From: norhanadel Date: Sun, 19 Oct 2025 19:05:22 +0300 Subject: [PATCH 3/5] edit location feature base back end api --- .../example/project/data/dto/DistrictDto.kt | 3 +- .../data/repository/LocationRepositoryImpl.kt | 16 ++-- .../data/repository/mapper/District.kt | 5 +- .../org/example/project/di/CraftoModule.kt | 1 + .../example/project/domain/entity/District.kt | 1 + .../presentation/components/Location.kt | 75 ++++++++++++++++ .../location/LocationSetupScreen.kt | 85 ++++--------------- .../setupScreens/location/LocationUiState.kt | 1 + .../location/LocationViewModel.kt | 9 +- 9 files changed, 113 insertions(+), 83 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt index 6ed53e3..93c9914 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/DistrictDto.kt @@ -6,5 +6,6 @@ import kotlinx.serialization.Serializable @Serializable data class DistrictDto( @SerialName("id") val id: String, - @SerialName("name") val name: String + @SerialName("name") val name: String, + @SerialName("governorateId") val governorateId: String ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt index 360ac85..5065857 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/LocationRepositoryImpl.kt @@ -1,3 +1,5 @@ +package org.example.project.data.repository + import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.call.body @@ -11,22 +13,20 @@ import org.example.project.data.utils.NetworkConstants.LOCATION_PATH import org.example.project.domain.entity.District import org.example.project.domain.entity.Governorates import org.example.project.domain.repository.LocationRepository -import org.koin.core.annotation.Provided -import org.koin.core.annotation.Single -internal class LocationRepositoryImpl( - private val httpClient: HttpClient +class LocationRepositoryImpl( + private val httpClient: HttpClient ) : LocationRepository { override suspend fun getAllGovernorates(): List { val response = httpClient.get("/$LOCATION_PATH/$GOVERNORATES_ENDPOINT") - val body: List = response.body() - return body.toGovernorates() - } + val body: List = response.body() + return body.toGovernorates() + } override suspend fun getDistrictsByGovernorateId(governorateId: String): List { val response = httpClient.get("/$LOCATION_PATH/$DISTRICT_ENDPOINT/$governorateId") - val body: List = response.body() + val body: List = response.body() return body.toDistrict() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt index 628892e..0c57291 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/District.kt @@ -7,7 +7,8 @@ fun List.toDistrict(): List { return map { dto -> District( id = dto.id, - name = dto.name + name = dto.name, + governorateId = dto.governorateId ) } -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt index b7aca13..91df6ba 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt @@ -1,3 +1,4 @@ +import org.example.project.data.repository.LocationRepositoryImpl import org.example.project.domain.repository.LocationRepository import org.koin.core.annotation.Module import org.koin.dsl.module diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt index 792f3d9..6d843cb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/District.kt @@ -3,4 +3,5 @@ package org.example.project.domain.entity data class District( val id: String, val name: String, + val governorateId: String ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt new file mode 100644 index 0000000..bdbdd58 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt @@ -0,0 +1,75 @@ +package org.example.project.presentation.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.* +import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun GovernorateSelector( + displayText: String, + hasSelection: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = AppTheme.craftoColors.background.card, + shape = RoundedCornerShape(AppTheme.craftoRadius.lg) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + hint = stringResource(Res.string.location_hint), + text = displayText, + onTextChange = {}, + modifier = Modifier.fillMaxWidth(), + startIcon = { + Icon( + painter = painterResource(Res.drawable.location), + contentDescription = stringResource(Res.string.location_icon), + tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary + ) + }, + endIcon = { + Icon( + painter = painterResource(Res.drawable.alt_arrow_down), + contentDescription = stringResource(Res.string.dropdown_icon), + tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary, + modifier = Modifier.clickable(onClick = onClick) + ) + } + ) + } +} + +@Composable +fun DetailLocationInput( + text: String, + onTextChange: (String) -> Unit +) { + TextField( + hint = stringResource(Res.string.enter_detailed_location), + text = text, + onTextChange = onTextChange, + modifier = Modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt index 24a667b..0b36310 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt @@ -8,12 +8,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets 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.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,10 +27,11 @@ import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.* import org.example.project.domain.entity.District import org.example.project.domain.entity.Governorates +import org.example.project.presentation.components.DetailLocationInput +import org.example.project.presentation.components.GovernorateSelector import org.example.project.presentation.designsystem.components.BottomSheet import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton -import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.designsystem.textstyle.AppTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -40,8 +39,6 @@ import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI -private val FieldHeight = 48.dp -private val DefaultPadding = 16.dp @OptIn(KoinExperimentalAPI::class) @Composable @@ -55,7 +52,7 @@ fun LocationSetupScreen( val displayText by remember(state.selectedGovernorate, state.selectedDistrict) { derivedStateOf { val parts = listOf(state.selectedGovernorate, state.selectedDistrict).filter { it.isNotBlank() } - if (parts.isEmpty())"Governorate, District" else parts.joinToString(", ") + if (parts.isEmpty()) "Governorate, District" else parts.joinToString(", ") } } @@ -64,9 +61,9 @@ fun LocationSetupScreen( .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) .background(AppTheme.craftoColors.background.screen) - .padding(DefaultPadding), + .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(DefaultPadding) + verticalArrangement = Arrangement.spacedBy(16.dp) ) { AccountSetupTopBar( modifier = Modifier.fillMaxWidth(), @@ -88,7 +85,10 @@ fun LocationSetupScreen( ErrorMessage(error = state.error) - NextButton(onClick = viewModel::onNextClick) + NextButton( + onClick = viewModel::onNextClick, + enabled = state.selectedGovernorate.isNotBlank() && state.selectedDistrict.isNotBlank() + ) } GovernorateBottomSheet( @@ -132,7 +132,7 @@ private fun LocationHeader(modifier: Modifier = Modifier) { text = stringResource(Res.string.location_title), style = AppTheme.textStyle.display, color = AppTheme.craftoColors.shade.primary, - modifier = Modifier.padding(bottom = DefaultPadding) + modifier = Modifier.padding(bottom = 16.dp) ) Text( @@ -143,59 +143,7 @@ private fun LocationHeader(modifier: Modifier = Modifier) { } } -@Composable -private fun GovernorateSelector( - displayText: String, - hasSelection: Boolean, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(FieldHeight) - .background( - color = AppTheme.craftoColors.background.card, - shape = RoundedCornerShape(AppTheme.craftoRadius.lg) - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - TextField( - hint = stringResource(Res.string.location_hint), - text = displayText, - onTextChange = {}, - modifier = Modifier.fillMaxWidth(), - startIcon = { - Icon( - painter = painterResource(Res.drawable.location), - contentDescription = stringResource(Res.string.location_icon), - tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary - ) - }, - endIcon = { - Icon( - painter = painterResource(Res.drawable.alt_arrow_down), - contentDescription = stringResource(Res.string.dropdown_icon), - tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary, - modifier = Modifier.clickable(onClick = onClick) - ) - } - ) - } -} -@Composable -private fun DetailLocationInput( - text: String, - onTextChange: (String) -> Unit -) { - TextField( - hint = stringResource(Res.string.enter_detailed_location), - text = text, - onTextChange = onTextChange, - modifier = Modifier.fillMaxWidth() - ) -} @Composable private fun ErrorMessage(error: String?) { @@ -210,12 +158,15 @@ private fun ErrorMessage(error: String?) { } @Composable -private fun NextButton(onClick: () -> Unit) { +private fun NextButton( + onClick: () -> Unit, + enabled: Boolean +) { PrimaryButton( text = stringResource(Res.string.next_button), - enabled = true, + enabled = enabled, onClick = onClick, - buttonState = ButtonState.Enable, + buttonState = if (enabled) ButtonState.Enable else ButtonState.DISABLED, modifier = Modifier.fillMaxWidth() ) } @@ -238,7 +189,7 @@ private fun GovernorateBottomSheet( modifier = Modifier .fillMaxWidth() .clickable { onSelect(governorate) } - .padding(DefaultPadding), + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -278,7 +229,7 @@ private fun DistrictBottomSheet( modifier = Modifier .fillMaxWidth() .clickable { onSelect(district.name) } - .padding(DefaultPadding), + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt index 0d890c8..6720e43 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt @@ -7,6 +7,7 @@ data class LocationUiState( val governorates: List = emptyList(), val districts: List = emptyList(), val selectedGovernorate: String = "", + val selectedGovernorateId: String = "", val selectedDistrict: String = "", val detailLocation: String = "", val isLoading: Boolean = false, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt index 11860d4..e7cadbc 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt @@ -10,15 +10,13 @@ class LocationViewModel( private val repository: LocationRepository ) : BaseViewModel(initialState = LocationUiState()) { - init { fetchGovernorates() } private fun fetchGovernorates() { tryToCall( - call = { - repository.getAllGovernorates() }, + call = { repository.getAllGovernorates() }, onSuccess = { governorates -> updateState { it.copy(governorates = governorates, isLoading = false, error = null) } }, @@ -32,8 +30,7 @@ class LocationViewModel( fun fetchDistricts(governorateId: String) { tryToCall( - call = { - repository.getDistrictsByGovernorateId(governorateId) }, + call = { repository.getDistrictsByGovernorateId(governorateId) }, onSuccess = { districts -> updateState { it.copy( @@ -51,10 +48,12 @@ class LocationViewModel( dispatcher = Dispatchers.IO ) } + fun selectGovernorate(governorate: Governorates) { updateState { it.copy( selectedGovernorate = governorate.name, + selectedGovernorateId = governorate.id, // Store governorate ID selectedDistrict = "", showGovernorateSheet = false ) From 0dea72c0f5c329f1dd58ca353cea4cdbe6ef7a17 Mon Sep 17 00:00:00 2001 From: norhanadel Date: Fri, 24 Oct 2025 18:14:53 +0300 Subject: [PATCH 4/5] edit location feature --- .../composeResources/values-ar/string.xml | 5 +- .../composeResources/values/string.xml | 6 +- .../components/DetailLocationInput.kt | 23 ++++ .../components/DropdownBottomSheet.kt | 57 ++++++++ .../components/DropdownSelector.kt | 74 ++++++++++ .../presentation/components/Location.kt | 13 -- .../location/LocationSetupScreen.kt | 127 +++--------------- .../setupScreens/location/LocationUiState.kt | 3 +- .../location/LocationViewModel.kt | 22 ++- 9 files changed, 202 insertions(+), 128 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DetailLocationInput.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownBottomSheet.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownSelector.kt diff --git a/composeApp/src/commonMain/composeResources/values-ar/string.xml b/composeApp/src/commonMain/composeResources/values-ar/string.xml index 66e457e..0e01b6b 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/string.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/string.xml @@ -34,7 +34,10 @@ التالي أيقونة الموقع أيقونة القائمة المنسدلة - أيقونة السهم أدخل موقعك بالتفصيل + العوده + السهم لأسفل + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/string.xml b/composeApp/src/commonMain/composeResources/values/string.xml index e60ad4c..2d14a09 100644 --- a/composeApp/src/commonMain/composeResources/values/string.xml +++ b/composeApp/src/commonMain/composeResources/values/string.xml @@ -43,7 +43,11 @@ Next Location icon Dropdown - Arrow Enter your location in detail + back arrow + back arrow + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DetailLocationInput.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DetailLocationInput.kt new file mode 100644 index 0000000..c27022b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DetailLocationInput.kt @@ -0,0 +1,23 @@ +package org.example.project.presentation.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.components.TextField +import org.jetbrains.compose.resources.stringResource +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.enter_detailed_location + +@Composable +fun DetailLocationInput( + text: String, + onTextChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + TextField( + hint = stringResource(Res.string.enter_detailed_location), + text = text, + onTextChange = onTextChange, + modifier = modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownBottomSheet.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownBottomSheet.kt new file mode 100644 index 0000000..a9df274 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownBottomSheet.kt @@ -0,0 +1,57 @@ +package org.example.project.presentation.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.example.project.presentation.designsystem.components.BottomSheet +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.alt_arrow_down +import crafto.composeapp.generated.resources.down_arrow + +@Composable +fun DropdownBottomSheet( + show: Boolean, + items: List, + itemLabel: (T) -> String, + onDismiss: () -> Unit, + onSelect: (T) -> Unit +) { + if (show && items.isNotEmpty()) { + BottomSheet(onDismissRequest = onDismiss) { + LazyColumn { + items(items) { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(item) } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = itemLabel(item), + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.shade.primary, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(Res.drawable.alt_arrow_down), + contentDescription = stringResource(Res.string.down_arrow), + tint = AppTheme.craftoColors.shade.secondary + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownSelector.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownSelector.kt new file mode 100644 index 0000000..79d0eda --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/DropdownSelector.kt @@ -0,0 +1,74 @@ +package org.example.project.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import crafto.composeapp.generated.resources.* + +@Composable +fun DropdownSelector( + text: String, + hint: String, + icon: DrawableResource, + hasSelection: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = AppTheme.craftoColors.background.card, + shape = RoundedCornerShape(AppTheme.craftoRadius.lg) + ) + .clickable( + onClick = onClick, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + .semantics { role = Role.Button }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextField( + hint = hint, + text = text, + onTextChange = {}, + readOnlyMode = true, + enabledState = false, + startIcon = { + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = if (hasSelection) AppTheme.craftoColors.shade.primary + else AppTheme.craftoColors.shade.tertiary + ) + }, + endIcon = { + Icon( + painter = painterResource(Res.drawable.alt_arrow_down), + contentDescription = stringResource(Res.string.dropdown_icon), + tint = if (hasSelection) AppTheme.craftoColors.shade.primary + else AppTheme.craftoColors.shade.tertiary + ) + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt index bdbdd58..d9ce05b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt @@ -60,16 +60,3 @@ fun GovernorateSelector( ) } } - -@Composable -fun DetailLocationInput( - text: String, - onTextChange: (String) -> Unit -) { - TextField( - hint = stringResource(Res.string.enter_detailed_location), - text = text, - onTextChange = onTextChange, - modifier = Modifier.fillMaxWidth() - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt index 0b36310..bef097b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt @@ -1,61 +1,44 @@ package org.example.project.presentation.screens.setupScreens.location import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.* -import org.example.project.domain.entity.District -import org.example.project.domain.entity.Governorates import org.example.project.presentation.components.DetailLocationInput -import org.example.project.presentation.components.GovernorateSelector -import org.example.project.presentation.designsystem.components.BottomSheet +import org.example.project.presentation.components.DropdownBottomSheet +import org.example.project.presentation.components.DropdownSelector import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI - @OptIn(KoinExperimentalAPI::class) @Composable fun LocationSetupScreen( - viewModel: LocationViewModel = koinViewModel(), + viewModel: LocationViewModel = koinViewModel() ) { val state by viewModel.state.collectAsState() HandleEffects(viewModel) - val displayText by remember(state.selectedGovernorate, state.selectedDistrict) { - derivedStateOf { - val parts = listOf(state.selectedGovernorate, state.selectedDistrict).filter { it.isNotBlank() } - if (parts.isEmpty()) "Governorate, District" else parts.joinToString(", ") - } - } - Column( modifier = Modifier .fillMaxSize() @@ -65,17 +48,17 @@ fun LocationSetupScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - AccountSetupTopBar( - modifier = Modifier.fillMaxWidth(), - currentPage = 2 - ) - + AccountSetupTopBar(modifier = Modifier.fillMaxWidth(), currentPage = 2) LocationHeader(modifier = Modifier.weight(1f)) - GovernorateSelector( - displayText = displayText, - hasSelection = displayText != stringResource(Res.string.location_hint), - onClick = viewModel::openGovernorateSheet + DropdownSelector( + text = state.locationDisplayText, + hint = stringResource(Res.string.location_hint), + icon = Res.drawable.location, + hasSelection = state.selectedGovernorate.isNotBlank() && state.selectedDistrict.isNotBlank(), + onClick = { + viewModel.openGovernorateSheet() + } ) DetailLocationInput( @@ -91,18 +74,20 @@ fun LocationSetupScreen( ) } - GovernorateBottomSheet( + DropdownBottomSheet( show = state.showGovernorateSheet, - governorates = state.governorates, + items = state.governorates, + itemLabel = { it.name }, onDismiss = viewModel::closeGovernorateSheet, onSelect = viewModel::selectGovernorate ) - DistrictBottomSheet( + DropdownBottomSheet( show = state.showDistrictSheet, - districts = state.districts, + items = state.districts, + itemLabel = { it.name }, onDismiss = viewModel::closeDistrictSheet, - onSelect = viewModel::selectDistrict + onSelect = { viewModel.selectDistrict(it.name) } ) } @@ -171,80 +156,6 @@ private fun NextButton( ) } -@Composable -private fun GovernorateBottomSheet( - show: Boolean, - governorates: List, - onDismiss: () -> Unit, - onSelect: (Governorates) -> Unit -) { - if (show) { - BottomSheet( - onDismissRequest = onDismiss - ) { - LazyColumn { - items(governorates.size) { index -> - val governorate = governorates[index] - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelect(governorate) } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = governorate.name, - style = AppTheme.textStyle.body.medium, - color = AppTheme.craftoColors.shade.primary, - modifier = Modifier.weight(1f) - ) - Icon( - painter = painterResource(Res.drawable.alt_arrow_down), - contentDescription = stringResource(Res.string.arrow_icon), - tint = AppTheme.craftoColors.shade.secondary - ) - } - } - } - } - } -} - -@Composable -private fun DistrictBottomSheet( - show: Boolean, - districts: List, - onDismiss: () -> Unit, - onSelect: (String) -> Unit -) { - if (show) { - BottomSheet( - onDismissRequest = onDismiss - ) { - LazyColumn { - items(districts.size) { index -> - val district = districts[index] - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelect(district.name) } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = district.name, - style = AppTheme.textStyle.body.medium, - color = AppTheme.craftoColors.shade.primary, - modifier = Modifier.weight(1f) - ) - } - } - } - } - } -} @Preview @Composable diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt index 6720e43..f403a7d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt @@ -13,5 +13,6 @@ data class LocationUiState( val isLoading: Boolean = false, val error: String? = null, val showGovernorateSheet: Boolean = false, - val showDistrictSheet: Boolean = false + val showDistrictSheet: Boolean = false, + val locationDisplayText: String = "Governorate, District" ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt index e7cadbc..1fea3cb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt @@ -12,8 +12,10 @@ class LocationViewModel( init { fetchGovernorates() + updateLocationDisplay() } + private fun fetchGovernorates() { tryToCall( call = { repository.getAllGovernorates() }, @@ -48,17 +50,20 @@ class LocationViewModel( dispatcher = Dispatchers.IO ) } - + fun updateDetailLocation(text: String) { + updateState { it.copy(detailLocation = text) } + } fun selectGovernorate(governorate: Governorates) { updateState { it.copy( selectedGovernorate = governorate.name, - selectedGovernorateId = governorate.id, // Store governorate ID + selectedGovernorateId = governorate.id, selectedDistrict = "", showGovernorateSheet = false ) } fetchDistricts(governorate.id) + updateLocationDisplay() } fun selectDistrict(district: String) { @@ -68,10 +73,19 @@ class LocationViewModel( showDistrictSheet = false ) } + updateLocationDisplay() } - fun updateDetailLocation(text: String) { - updateState { it.copy(detailLocation = text) } + private fun updateLocationDisplay() { + updateState { currentState -> + val parts = listOfNotNull( + currentState.selectedGovernorate.takeIf { it.isNotBlank() }, + currentState.selectedDistrict.takeIf { it.isNotBlank() } + ) + val displayText = parts.joinToString(", ") + + currentState.copy(locationDisplayText = displayText) + } } fun openGovernorateSheet() { From 4f39840fa379aa2bc072a069a0696e03d282b047 Mon Sep 17 00:00:00 2001 From: norhanadel Date: Fri, 24 Oct 2025 18:15:20 +0300 Subject: [PATCH 5/5] edit location feature --- .../presentation/components/Location.kt | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt deleted file mode 100644 index d9ce05b..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/components/Location.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.example.project.presentation.components - -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.ui.Alignment -import androidx.compose.ui.unit.dp -import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.* -import org.example.project.presentation.designsystem.components.TextField -import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource - -@Composable -fun GovernorateSelector( - displayText: String, - hasSelection: Boolean, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .background( - color = AppTheme.craftoColors.background.card, - shape = RoundedCornerShape(AppTheme.craftoRadius.lg) - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - TextField( - hint = stringResource(Res.string.location_hint), - text = displayText, - onTextChange = {}, - modifier = Modifier.fillMaxWidth(), - startIcon = { - Icon( - painter = painterResource(Res.drawable.location), - contentDescription = stringResource(Res.string.location_icon), - tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary - ) - }, - endIcon = { - Icon( - painter = painterResource(Res.drawable.alt_arrow_down), - contentDescription = stringResource(Res.string.dropdown_icon), - tint = if (hasSelection) AppTheme.craftoColors.shade.primary else AppTheme.craftoColors.shade.tertiary, - modifier = Modifier.clickable(onClick = onClick) - ) - } - ) - } -}