diff --git a/ui/designsystem/src/main/kotlin/dev/adriankuta/flights/ui/designsystem/theme/PreviewDevices.kt b/ui/designsystem/src/main/kotlin/dev/adriankuta/flights/ui/designsystem/theme/PreviewDevices.kt index 4af3c03..0b921a3 100644 --- a/ui/designsystem/src/main/kotlin/dev/adriankuta/flights/ui/designsystem/theme/PreviewDevices.kt +++ b/ui/designsystem/src/main/kotlin/dev/adriankuta/flights/ui/designsystem/theme/PreviewDevices.kt @@ -2,8 +2,8 @@ package dev.adriankuta.flights.ui.designsystem.theme import androidx.compose.ui.tooling.preview.Preview -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480", showBackground = true) +@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480", showBackground = true) +@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480", showBackground = true) +@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480", showBackground = true) annotation class PreviewDevices diff --git a/ui/home/build.gradle.kts b/ui/home/build.gradle.kts index d0e1212..bf07e04 100644 --- a/ui/home/build.gradle.kts +++ b/ui/home/build.gradle.kts @@ -11,8 +11,9 @@ android { dependencies { implementation(projects.core.util) implementation(projects.ui.designsystem) + implementation(projects.ui.sharedui) implementation(projects.domain.search) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.timber) -} \ No newline at end of file +} diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt index 6a1268b..f50ce57 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt @@ -1,11 +1,16 @@ package dev.adriankuta.flights.ui.home +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -22,6 +27,7 @@ import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices import dev.adriankuta.flights.ui.home.components.AirportDropdown import dev.adriankuta.flights.ui.home.components.DatePicker +import dev.adriankuta.flights.ui.sharedui.Counter import java.time.LocalDate @Composable @@ -35,6 +41,9 @@ internal fun HomeScreen( onOriginAirportSelect = viewModel::selectOriginAirport, onDestinationAirportSelect = viewModel::selectDestinationAirport, onDateSelect = viewModel::selectDate, + onAdultCountChange = viewModel::updateAdultCount, + onTeenCountChange = viewModel::updateTeenCount, + onChildCountChange = viewModel::updateChildCount, ) } @@ -44,6 +53,9 @@ private fun HomeScreen( onOriginAirportSelect: (AirportInfo) -> Unit, onDestinationAirportSelect: (AirportInfo) -> Unit, onDateSelect: (LocalDate) -> Unit, + onAdultCountChange: (Int) -> Unit, + onTeenCountChange: (Int) -> Unit, + onChildCountChange: (Int) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -52,40 +64,98 @@ private fun HomeScreen( when (uiState) { is HomeUiState.Error -> Text("Error") HomeUiState.Loading -> Text("Loading") - is HomeUiState.Success -> { - AirportDropdown( - label = "Origin Airport", - airports = uiState.originAirports, - selectedAirport = uiState.selectedOriginAirport, - onAirportSelect = onOriginAirportSelect, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - AirportDropdown( - label = "Destination Airport", - airports = uiState.destinationAirports, - selectedAirport = uiState.selectedDestinationAirport, - onAirportSelect = onDestinationAirportSelect, - modifier = Modifier.fillMaxWidth(), - enabled = uiState.selectedOriginAirport != null, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - DatePicker( - label = "Departure Date", - selectedDate = uiState.selectedDate, - onDateSelect = onDateSelect, - modifier = Modifier.fillMaxWidth(), - enabled = uiState.selectedDestinationAirport != null, - ) - } + is HomeUiState.Success -> SearchForm( + uiState = uiState, + onOriginAirportSelect = onOriginAirportSelect, + onDestinationAirportSelect = onDestinationAirportSelect, + onDateSelect = onDateSelect, + onAdultCountChange = onAdultCountChange, + onTeenCountChange = onTeenCountChange, + onChildCountChange = onChildCountChange, + ) } } } +@Composable +private fun ColumnScope.SearchForm( + uiState: HomeUiState.Success, + onOriginAirportSelect: (AirportInfo) -> Unit, + onDestinationAirportSelect: (AirportInfo) -> Unit, + onDateSelect: (LocalDate) -> Unit, + onAdultCountChange: (Int) -> Unit, + onTeenCountChange: (Int) -> Unit, + onChildCountChange: (Int) -> Unit, +) { + AirportDropdown( + label = "Origin Airport", + airports = uiState.originAirports, + selectedAirport = uiState.selectedOriginAirport, + onAirportSelect = onOriginAirportSelect, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + AirportDropdown( + label = "Destination Airport", + airports = uiState.destinationAirports, + selectedAirport = uiState.selectedDestinationAirport, + onAirportSelect = onDestinationAirportSelect, + modifier = Modifier.fillMaxWidth(), + enabled = uiState.selectedOriginAirport != null, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DatePicker( + label = "Departure Date", + selectedDate = uiState.selectedDate, + onDateSelect = onDateSelect, + modifier = Modifier.fillMaxWidth(), + enabled = uiState.selectedDestinationAirport != null, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text("Passengers") + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Counter( + value = uiState.passengers.adultCount, + onCountChange = onAdultCountChange, + label = "Adults", + modifier = Modifier.weight(1f), + minVal = 1, + ) + + VerticalDivider() + + Counter( + value = uiState.passengers.teenCount, + onCountChange = onTeenCountChange, + label = "Teens", + modifier = Modifier.weight(1f), + ) + + VerticalDivider() + + Counter( + value = uiState.passengers.childCount, + onCountChange = onChildCountChange, + label = "Children", + modifier = Modifier.weight(1f), + ) + } +} + @PreviewDevices @Composable private fun HomeScreenLoadingPreview() { @@ -95,6 +165,9 @@ private fun HomeScreenLoadingPreview() { onOriginAirportSelect = {}, onDestinationAirportSelect = {}, onDateSelect = {}, + onAdultCountChange = {}, + onTeenCountChange = {}, + onChildCountChange = {}, ) } } @@ -137,10 +210,18 @@ private fun HomeScreenSuccessPreview() { selectedOriginAirport = mockAirports.first(), selectedDestinationAirport = mockAirports.last(), selectedDate = LocalDate.now(), + passengers = PassengersState( + adultCount = 2, + teenCount = 1, + childCount = 1, + ), ), onOriginAirportSelect = {}, onDestinationAirportSelect = {}, onDateSelect = {}, + onAdultCountChange = {}, + onTeenCountChange = {}, + onChildCountChange = {}, ) } } diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt index 0ae4704..2673a65 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt @@ -10,6 +10,7 @@ import dev.adriankuta.flights.domain.types.AirportInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import java.time.LocalDate @@ -23,12 +24,20 @@ class HomeScreenViewModel @Inject constructor( private val selectedOriginAirport = MutableStateFlow(null) private val selectedDestinationAirport = MutableStateFlow(null) private val selectedDate = MutableStateFlow(LocalDate.now()) + private val adultCount = MutableStateFlow(1) + private val teenCount = MutableStateFlow(0) + private val childCount = MutableStateFlow(0) internal val uiState = homeUiState( useCase = observeAirportsUseCase, selectedOriginAirport = selectedOriginAirport, selectedDestinationAirport = selectedDestinationAirport, selectedDate = selectedDate, + passengersState = passengersState( + adultCount = adultCount, + teenCount = teenCount, + childCount = childCount, + ), ) .stateIn( scope = viewModelScope, @@ -61,20 +70,37 @@ class HomeScreenViewModel @Inject constructor( fun selectDate(date: LocalDate) { selectedDate.value = date } + + fun updateAdultCount(diff: Int) { + val newCount = adultCount.value + diff + adultCount.value = newCount + } + + fun updateTeenCount(diff: Int) { + val newCount = teenCount.value + diff + teenCount.value = newCount + } + + fun updateChildCount(diff: Int) { + val newCount = childCount.value + diff + childCount.value = newCount + } } private fun homeUiState( useCase: ObserveAirportsUseCase, - selectedOriginAirport: MutableStateFlow, - selectedDestinationAirport: MutableStateFlow, - selectedDate: MutableStateFlow, + selectedOriginAirport: StateFlow, + selectedDestinationAirport: StateFlow, + selectedDate: StateFlow, + passengersState: Flow, ): Flow { return combine( useCase().asResult(), selectedOriginAirport, selectedDestinationAirport, selectedDate, - ) { result, origin, destination, date -> + passengersState, + ) { result, origin, destination, date, passengers -> when (result) { is Result.Error -> HomeUiState.Error(result.exception) is Result.Loading -> HomeUiState.Loading @@ -86,12 +112,31 @@ private fun homeUiState( selectedOriginAirport = origin, selectedDestinationAirport = destination, selectedDate = date, + passengers = passengers, ) } } } } +private fun passengersState( + adultCount: StateFlow, + teenCount: StateFlow, + childCount: StateFlow, +): Flow { + return combine( + adultCount, + teenCount, + childCount, + ) { adults, teens, children -> + PassengersState( + adultCount = adults, + teenCount = teens, + childCount = children, + ) + } +} + internal sealed interface HomeUiState { data object Loading : HomeUiState data class Success( @@ -100,7 +145,14 @@ internal sealed interface HomeUiState { val selectedOriginAirport: AirportInfo? = null, val selectedDestinationAirport: AirportInfo? = null, val selectedDate: LocalDate = LocalDate.now(), + val passengers: PassengersState, ) : HomeUiState data class Error(val exception: Throwable) : HomeUiState } + +internal data class PassengersState( + val adultCount: Int, + val teenCount: Int, + val childCount: Int, +) diff --git a/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt b/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt index 6d4d551..04edae6 100644 --- a/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt +++ b/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt @@ -37,6 +37,7 @@ fun Counter( onCountChange: (diff: Int) -> Unit, modifier: Modifier = Modifier, label: String? = null, + minVal: Int = 0, ) { val view = LocalView.current @@ -86,11 +87,12 @@ fun Counter( onCountChange(-1) }, shape = MaterialTheme.shapes.medium, - enabled = value > 0, + enabled = value > minVal, ) { Text( text = "-", style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, ) } Button( @@ -105,6 +107,7 @@ fun Counter( Text( text = "+", style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, ) } }