mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-02 01:37:58 +02:00
Refactor: Migrate to kotlinx-datetime and implement flight search results display
This commit migrates date handling from `java.time` to `kotlinx-datetime` across various modules. It also introduces the display of flight search results on the home screen. Key changes: - Replaced `java.time.LocalDate` and related classes with `kotlinx.datetime.LocalDate` in: - `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/DatePicker.kt` - `model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/GetFlightsSearchContentUseCaseImpl.kt` - `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt` - `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt` - `domain/types/src/main/kotlin/dev/adriankuta/flights/domain/types/TripDate.kt` - `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/PassengersOptions.kt` - `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchForm.kt` - `domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/entities/SearchOptions.kt` - Added `kotlinx-datetime` dependency to `build-logic/convention/src/main/kotlin/dev/adriankuta/partymania/ConfigureKotlinAndroid.kt`. - Implemented `SearchResults.kt` composable to display flight search results. - Updated `HomeScreen.kt` to show `SearchResults` when results are available and handle back navigation. - Modified `HomeScreenViewModel.kt`: - Introduced `SearchResultUiState` to manage search result states (Loading, Success, Error, Idle). - Updated `search()` function to fetch and expose flight results. - Adjusted initial airport list in `homeUiState` to filter for "DUB" and "STN" and ensure destination isn't the same as origin. - Updated `FlightDomainMapper.kt` to parse date strings into `kotlinx.datetime.LocalDate`. - Added `HomeUiStatePreviewParameterProvider.kt` and `FlightPreviewParameterProvider.kt` for Compose previews. - Removed Timber dependency from `ui/home`, `domain/search`, and `domain/types` modules as it's now provided via convention plugin.
This commit is contained in:
@ -61,6 +61,7 @@ internal fun Project.configureKotlinAndroid(
|
|||||||
dependencies {
|
dependencies {
|
||||||
"implementation"(libs.findLibrary("androidx.core.ktx").get())
|
"implementation"(libs.findLibrary("androidx.core.ktx").get())
|
||||||
"implementation"(libs.findLibrary("kotlinx.coroutines.android").get())
|
"implementation"(libs.findLibrary("kotlinx.coroutines.android").get())
|
||||||
|
"implementation"(libs.findLibrary("kotlinx.datetime").get())
|
||||||
"implementation"(libs.findLibrary("timber").get())
|
"implementation"(libs.findLibrary("timber").get())
|
||||||
|
|
||||||
"coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
|
"coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
|
||||||
|
@ -9,5 +9,4 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.domain.types)
|
api(projects.domain.types)
|
||||||
implementation(libs.timber)
|
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package dev.adriankuta.flights.domain.search.entities
|
package dev.adriankuta.flights.domain.search.entities
|
||||||
|
|
||||||
import dev.adriankuta.flights.domain.types.Airport
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
import java.time.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
data class SearchOptions(
|
data class SearchOptions(
|
||||||
val origin: Airport.Departure,
|
val origin: Airport.Departure,
|
||||||
|
@ -9,5 +9,4 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.util)
|
implementation(projects.core.util)
|
||||||
implementation(libs.timber)
|
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package dev.adriankuta.flights.domain.types
|
package dev.adriankuta.flights.domain.types
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
data class TripDate(
|
data class TripDate(
|
||||||
val dateOut: String,
|
val dateOut: LocalDate?,
|
||||||
val flights: List<TripFlight>,
|
val flights: List<TripFlight>,
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ import dev.adriankuta.flights.domain.types.TripDate
|
|||||||
import dev.adriankuta.flights.domain.types.TripFare
|
import dev.adriankuta.flights.domain.types.TripFare
|
||||||
import dev.adriankuta.flights.domain.types.TripFlight
|
import dev.adriankuta.flights.domain.types.TripFlight
|
||||||
import dev.adriankuta.model.data.api.entities.FlightResponse
|
import dev.adriankuta.model.data.api.entities.FlightResponse
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
import dev.adriankuta.model.data.api.entities.RegularFare as ApiRegularFare
|
import dev.adriankuta.model.data.api.entities.RegularFare as ApiRegularFare
|
||||||
import dev.adriankuta.model.data.api.entities.Segment as ApiSegment
|
import dev.adriankuta.model.data.api.entities.Segment as ApiSegment
|
||||||
import dev.adriankuta.model.data.api.entities.Trip as ApiTrip
|
import dev.adriankuta.model.data.api.entities.Trip as ApiTrip
|
||||||
@ -33,7 +34,7 @@ internal fun ApiTrip.toDomain(): Trip {
|
|||||||
|
|
||||||
internal fun ApiTripDate.toDomain(): TripDate {
|
internal fun ApiTripDate.toDomain(): TripDate {
|
||||||
return TripDate(
|
return TripDate(
|
||||||
dateOut = dateOut ?: "",
|
dateOut = dateOut?.let { LocalDateTime.parse(it).date },
|
||||||
flights = flights.orEmpty().map { it.toDomain() },
|
flights = flights.orEmpty().map { it.toDomain() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import dev.adriankuta.flights.domain.search.entities.SearchOptions
|
|||||||
import dev.adriankuta.flights.domain.types.Flight
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
import dev.adriankuta.flights.model.repository.mappers.toDomain
|
import dev.adriankuta.flights.model.repository.mappers.toDomain
|
||||||
import dev.adriankuta.model.data.api.FlightService
|
import dev.adriankuta.model.data.api.FlightService
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class GetFlightsSearchContentUseCaseImpl @Inject constructor(
|
internal class GetFlightsSearchContentUseCaseImpl @Inject constructor(
|
||||||
@ -16,7 +15,7 @@ internal class GetFlightsSearchContentUseCaseImpl @Inject constructor(
|
|||||||
val result = flightService.getFlights(
|
val result = flightService.getFlights(
|
||||||
origin = searchOptions.origin.code,
|
origin = searchOptions.origin.code,
|
||||||
destination = searchOptions.destination.code,
|
destination = searchOptions.destination.code,
|
||||||
date = searchOptions.date.format(DateTimeFormatter.ISO_DATE),
|
date = searchOptions.date.toString(),
|
||||||
adult = searchOptions.adults,
|
adult = searchOptions.adults,
|
||||||
teen = searchOptions.teens,
|
teen = searchOptions.teens,
|
||||||
child = searchOptions.children,
|
child = searchOptions.children,
|
||||||
|
@ -15,5 +15,4 @@ dependencies {
|
|||||||
implementation(projects.domain.search)
|
implementation(projects.domain.search)
|
||||||
|
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.timber)
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package dev.adriankuta.flights.ui.home
|
package dev.adriankuta.flights.ui.home
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@ -7,29 +8,39 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
import dev.adriankuta.flights.domain.types.City
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
import dev.adriankuta.flights.domain.types.Coordinates
|
import dev.adriankuta.flights.domain.types.RegularFare
|
||||||
import dev.adriankuta.flights.domain.types.Country
|
import dev.adriankuta.flights.domain.types.Segment
|
||||||
import dev.adriankuta.flights.domain.types.MacCity
|
import dev.adriankuta.flights.domain.types.Trip
|
||||||
import dev.adriankuta.flights.domain.types.Region
|
import dev.adriankuta.flights.domain.types.TripDate
|
||||||
|
import dev.adriankuta.flights.domain.types.TripFare
|
||||||
|
import dev.adriankuta.flights.domain.types.TripFlight
|
||||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||||
import dev.adriankuta.flights.ui.home.components.SearchForm
|
import dev.adriankuta.flights.ui.home.components.SearchForm
|
||||||
import java.time.LocalDate
|
import dev.adriankuta.flights.ui.home.components.SearchResults
|
||||||
|
import dev.adriankuta.flights.ui.home.sample.HomeUiStatePreviewParameterProvider
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeScreen(
|
internal fun HomeScreen(
|
||||||
viewModel: HomeScreenViewModel = hiltViewModel(),
|
viewModel: HomeScreenViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val homeUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val homeUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val searchResults by viewModel.searchResultUiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
uiState = homeUiState,
|
uiState = homeUiState,
|
||||||
|
searchResultsUiState = searchResults,
|
||||||
onOriginAirportSelect = viewModel::selectOriginAirport,
|
onOriginAirportSelect = viewModel::selectOriginAirport,
|
||||||
onDestinationAirportSelect = viewModel::selectDestinationAirport,
|
onDestinationAirportSelect = viewModel::selectDestinationAirport,
|
||||||
onDateSelect = viewModel::selectDate,
|
onDateSelect = viewModel::selectDate,
|
||||||
@ -43,6 +54,7 @@ internal fun HomeScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreen(
|
private fun HomeScreen(
|
||||||
uiState: HomeUiState,
|
uiState: HomeUiState,
|
||||||
|
searchResultsUiState: SearchResultUiState,
|
||||||
onOriginAirportSelect: (AirportInfo) -> Unit,
|
onOriginAirportSelect: (AirportInfo) -> Unit,
|
||||||
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
||||||
onDateSelect: (LocalDate) -> Unit,
|
onDateSelect: (LocalDate) -> Unit,
|
||||||
@ -52,18 +64,25 @@ private fun HomeScreen(
|
|||||||
onSearch: () -> Unit,
|
onSearch: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
var showSearchResults by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
BackHandler(enabled = showSearchResults) {
|
||||||
|
showSearchResults = false
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp),
|
||||||
.verticalScroll(
|
|
||||||
state = scrollState,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
is HomeUiState.Error -> Text("Error")
|
is HomeUiState.Error -> Text("Error")
|
||||||
HomeUiState.Loading -> Text("Loading")
|
HomeUiState.Loading -> Text("Loading")
|
||||||
is HomeUiState.Success -> SearchForm(
|
is HomeUiState.Success -> if (showSearchResults) {
|
||||||
|
SearchResults(
|
||||||
|
uiState = searchResultsUiState,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SearchForm(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onOriginAirportSelect = onOriginAirportSelect,
|
onOriginAirportSelect = onOriginAirportSelect,
|
||||||
onDestinationAirportSelect = onDestinationAirportSelect,
|
onDestinationAirportSelect = onDestinationAirportSelect,
|
||||||
@ -71,10 +90,15 @@ private fun HomeScreen(
|
|||||||
onAdultCountChange = onAdultCountChange,
|
onAdultCountChange = onAdultCountChange,
|
||||||
onTeenCountChange = onTeenCountChange,
|
onTeenCountChange = onTeenCountChange,
|
||||||
onChildCountChange = onChildCountChange,
|
onChildCountChange = onChildCountChange,
|
||||||
onSearch = onSearch,
|
onSearch = {
|
||||||
|
showSearchResults = true
|
||||||
|
onSearch()
|
||||||
|
},
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewDevices
|
@PreviewDevices
|
||||||
@ -83,6 +107,7 @@ private fun HomeScreenLoadingPreview() {
|
|||||||
FlightsTheme {
|
FlightsTheme {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
uiState = HomeUiState.Loading,
|
uiState = HomeUiState.Loading,
|
||||||
|
searchResultsUiState = SearchResultUiState.Idle,
|
||||||
onOriginAirportSelect = {},
|
onOriginAirportSelect = {},
|
||||||
onDestinationAirportSelect = {},
|
onDestinationAirportSelect = {},
|
||||||
onDateSelect = {},
|
onDateSelect = {},
|
||||||
@ -96,48 +121,83 @@ private fun HomeScreenLoadingPreview() {
|
|||||||
|
|
||||||
@PreviewDevices
|
@PreviewDevices
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreenSuccessPreview() {
|
private fun HomeScreenSuccessPreview(
|
||||||
|
@PreviewParameter(HomeUiStatePreviewParameterProvider::class) homeUiState: HomeUiState,
|
||||||
|
) {
|
||||||
FlightsTheme {
|
FlightsTheme {
|
||||||
val mockAirports = listOf(
|
|
||||||
AirportInfo(
|
|
||||||
code = "WAW",
|
|
||||||
name = "Warsaw Chopin Airport",
|
|
||||||
seoName = "warsaw",
|
|
||||||
isBase = true,
|
|
||||||
timeZone = "Europe/Warsaw",
|
|
||||||
city = City("WAW", "Warsaw"),
|
|
||||||
macCity = MacCity("WARSAW"),
|
|
||||||
region = Region("WARSAW_PL", "Warsaw"),
|
|
||||||
country = Country("PL", "Poland", "PLN"),
|
|
||||||
coordinates = Coordinates(52.1657, 20.9671),
|
|
||||||
),
|
|
||||||
AirportInfo(
|
|
||||||
code = "KRK",
|
|
||||||
name = "Krakow Airport",
|
|
||||||
seoName = "krakow",
|
|
||||||
isBase = true,
|
|
||||||
timeZone = "Europe/Warsaw",
|
|
||||||
city = City("KRK", "Krakow"),
|
|
||||||
macCity = MacCity("KRAKOW"),
|
|
||||||
region = Region("KRAKOW_PL", "Krakow"),
|
|
||||||
country = Country("PL", "Poland", "PLN"),
|
|
||||||
coordinates = Coordinates(50.0777, 19.7848),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
uiState = HomeUiState.Success(
|
uiState = homeUiState,
|
||||||
originAirports = mockAirports,
|
searchResultsUiState = SearchResultUiState.Idle,
|
||||||
destinationAirports = mockAirports,
|
onOriginAirportSelect = {},
|
||||||
selectedOriginAirport = mockAirports.first(),
|
onDestinationAirportSelect = {},
|
||||||
selectedDestinationAirport = mockAirports.last(),
|
onDateSelect = {},
|
||||||
selectedDate = LocalDate.now(),
|
onAdultCountChange = {},
|
||||||
passengers = PassengersState(
|
onTeenCountChange = {},
|
||||||
adultCount = 2,
|
onChildCountChange = {},
|
||||||
teenCount = 1,
|
onSearch = {},
|
||||||
childCount = 1,
|
)
|
||||||
),
|
}
|
||||||
),
|
}
|
||||||
|
|
||||||
|
@PreviewDevices
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
private fun HomeScreenSearchResultsPreview(
|
||||||
|
@PreviewParameter(HomeUiStatePreviewParameterProvider::class) homeUiState: HomeUiState,
|
||||||
|
) {
|
||||||
|
val mockFlight = Flight(
|
||||||
|
currency = "PLN",
|
||||||
|
currPrecision = 2,
|
||||||
|
trips = listOf<Trip>(
|
||||||
|
Trip(
|
||||||
|
origin = "WAW",
|
||||||
|
destination = "KRK",
|
||||||
|
dates = listOf<TripDate>(
|
||||||
|
TripDate(
|
||||||
|
dateOut = LocalDate(2023, 6, 15),
|
||||||
|
flights = listOf(
|
||||||
|
TripFlight(
|
||||||
|
faresLeft = 10,
|
||||||
|
regularFare = RegularFare(
|
||||||
|
fares = listOf(
|
||||||
|
TripFare(
|
||||||
|
type = "ADT",
|
||||||
|
amount = 150.0,
|
||||||
|
count = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flightNumber = "FR1234",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-15T10:00:00",
|
||||||
|
"2023-06-15T11:30:00",
|
||||||
|
),
|
||||||
|
duration = "1h 30m",
|
||||||
|
segments = listOf(
|
||||||
|
Segment(
|
||||||
|
origin = "WAW",
|
||||||
|
destination = "KRK",
|
||||||
|
flightNumber = "FR1234",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-15T10:00:00",
|
||||||
|
"2023-06-15T11:30:00",
|
||||||
|
),
|
||||||
|
duration = "1h 30m",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
operatedBy = "Ryanair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
FlightsTheme {
|
||||||
|
HomeScreen(
|
||||||
|
uiState = homeUiState,
|
||||||
|
searchResultsUiState = SearchResultUiState.Success(mockFlight),
|
||||||
onOriginAirportSelect = {},
|
onOriginAirportSelect = {},
|
||||||
onDestinationAirportSelect = {},
|
onDestinationAirportSelect = {},
|
||||||
onDateSelect = {},
|
onDateSelect = {},
|
||||||
|
@ -10,6 +10,7 @@ import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
|||||||
import dev.adriankuta.flights.domain.search.entities.SearchOptions
|
import dev.adriankuta.flights.domain.search.entities.SearchOptions
|
||||||
import dev.adriankuta.flights.domain.types.Airport
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@ -17,8 +18,10 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import kotlinx.datetime.Clock
|
||||||
import java.time.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@ -29,10 +32,12 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
|
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
private val selectedDate = MutableStateFlow(LocalDate.now())
|
private val selectedDate =
|
||||||
|
MutableStateFlow(Clock.System.todayIn(TimeZone.currentSystemDefault()))
|
||||||
private val adultCount = MutableStateFlow(1)
|
private val adultCount = MutableStateFlow(1)
|
||||||
private val teenCount = MutableStateFlow(0)
|
private val teenCount = MutableStateFlow(0)
|
||||||
private val childCount = MutableStateFlow(0)
|
private val childCount = MutableStateFlow(0)
|
||||||
|
private val searchResults = MutableStateFlow<SearchResultUiState>(SearchResultUiState.Idle)
|
||||||
|
|
||||||
internal val uiState = homeUiState(
|
internal val uiState = homeUiState(
|
||||||
useCase = observeAirportsUseCase,
|
useCase = observeAirportsUseCase,
|
||||||
@ -51,6 +56,13 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
initialValue = HomeUiState.Loading,
|
initialValue = HomeUiState.Loading,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal val searchResultUiState = searchResults
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = SearchResultUiState.Idle,
|
||||||
|
)
|
||||||
|
|
||||||
fun selectOriginAirport(airport: AirportInfo) {
|
fun selectOriginAirport(airport: AirportInfo) {
|
||||||
selectedOriginAirport.value = airport
|
selectedOriginAirport.value = airport
|
||||||
// If the selected destination is the same as the new origin, clear the destination
|
// If the selected destination is the same as the new origin, clear the destination
|
||||||
@ -91,27 +103,39 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
|
searchResults.value = SearchResultUiState.Loading
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
val results = getFlightsSearchContentUseCase(
|
val results = getFlightsSearchContentUseCase(
|
||||||
searchOptions = SearchOptions(
|
searchOptions = prepareSearchOptions(),
|
||||||
|
)
|
||||||
|
searchResults.value = SearchResultUiState.Success(results)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
searchResults.value = SearchResultUiState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareSearchOptions(): SearchOptions {
|
||||||
|
val selectedOrigin = requireNotNull(selectedOriginAirport.value)
|
||||||
|
val selectedDestination = requireNotNull(selectedDestinationAirport.value)
|
||||||
|
|
||||||
|
return SearchOptions(
|
||||||
origin = Airport.Departure(
|
origin = Airport.Departure(
|
||||||
code = selectedOriginAirport.value?.code ?: return@launch,
|
code = selectedOrigin.code,
|
||||||
name = selectedOriginAirport.value?.name ?: return@launch,
|
name = selectedOrigin.name,
|
||||||
macCity = selectedOriginAirport.value?.macCity,
|
macCity = selectedOrigin.macCity,
|
||||||
),
|
),
|
||||||
destination = Airport.Arrival(
|
destination = Airport.Arrival(
|
||||||
code = selectedDestinationAirport.value?.code ?: return@launch,
|
code = selectedDestination.code,
|
||||||
name = selectedDestinationAirport.value?.name ?: return@launch,
|
name = selectedDestination.name,
|
||||||
macCity = selectedDestinationAirport.value?.macCity,
|
macCity = selectedDestination.macCity,
|
||||||
),
|
),
|
||||||
date = selectedDate.value,
|
date = selectedDate.value,
|
||||||
adults = adultCount.value,
|
adults = adultCount.value,
|
||||||
teens = teenCount.value,
|
teens = teenCount.value,
|
||||||
children = childCount.value,
|
children = childCount.value,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
Timber.d("Result $results")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,13 +196,20 @@ internal sealed interface HomeUiState {
|
|||||||
val destinationAirports: List<AirportInfo>,
|
val destinationAirports: List<AirportInfo>,
|
||||||
val selectedOriginAirport: AirportInfo? = null,
|
val selectedOriginAirport: AirportInfo? = null,
|
||||||
val selectedDestinationAirport: AirportInfo? = null,
|
val selectedDestinationAirport: AirportInfo? = null,
|
||||||
val selectedDate: LocalDate = LocalDate.now(),
|
val selectedDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
|
||||||
val passengers: PassengersState,
|
val passengers: PassengersState,
|
||||||
) : HomeUiState
|
) : HomeUiState
|
||||||
|
|
||||||
data class Error(val exception: Throwable) : HomeUiState
|
data class Error(val exception: Throwable) : HomeUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed interface SearchResultUiState {
|
||||||
|
data object Loading : SearchResultUiState
|
||||||
|
data class Success(val flight: Flight) : SearchResultUiState
|
||||||
|
data class Error(val exception: Throwable) : SearchResultUiState
|
||||||
|
data object Idle : SearchResultUiState
|
||||||
|
}
|
||||||
|
|
||||||
internal data class PassengersState(
|
internal data class PassengersState(
|
||||||
val adultCount: Int,
|
val adultCount: Int,
|
||||||
val teenCount: Int,
|
val teenCount: Int,
|
||||||
|
@ -24,10 +24,16 @@ import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
|||||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import java.time.Instant
|
import kotlinx.datetime.Clock
|
||||||
import java.time.LocalDate
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import java.time.ZoneId
|
import kotlinx.datetime.Instant
|
||||||
import java.time.ZoneOffset
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.atStartOfDayIn
|
||||||
|
import kotlinx.datetime.plus
|
||||||
|
import kotlinx.datetime.toJavaLocalDate
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -45,8 +51,8 @@ fun DatePicker(
|
|||||||
|
|
||||||
// Ensure the selected date is not in the past
|
// Ensure the selected date is not in the past
|
||||||
val validatedDate = remember(selectedDate) {
|
val validatedDate = remember(selectedDate) {
|
||||||
if (selectedDate.isBefore(LocalDate.now())) {
|
if (selectedDate < Clock.System.todayIn(TimeZone.currentSystemDefault())) {
|
||||||
LocalDate.now()
|
Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
} else {
|
} else {
|
||||||
selectedDate
|
selectedDate
|
||||||
}
|
}
|
||||||
@ -54,7 +60,7 @@ fun DatePicker(
|
|||||||
|
|
||||||
// Format the date for display
|
// Format the date for display
|
||||||
val formattedDate = remember(validatedDate) {
|
val formattedDate = remember(validatedDate) {
|
||||||
validatedDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
validatedDate.toJavaLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
||||||
}
|
}
|
||||||
val interactionSource = remember {
|
val interactionSource = remember {
|
||||||
object : MutableInteractionSource {
|
object : MutableInteractionSource {
|
||||||
@ -93,9 +99,8 @@ fun DatePicker(
|
|||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
val datePickerState = rememberDatePickerState(
|
val datePickerState = rememberDatePickerState(
|
||||||
initialSelectedDateMillis = selectedDate
|
initialSelectedDateMillis = selectedDate
|
||||||
.atStartOfDay(ZoneId.systemDefault())
|
.atStartOfDayIn(TimeZone.currentSystemDefault())
|
||||||
.toInstant()
|
.toEpochMilliseconds(),
|
||||||
.toEpochMilli(),
|
|
||||||
selectableDates = FutureSelectableDates(),
|
selectableDates = FutureSelectableDates(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -109,11 +114,11 @@ fun DatePicker(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
datePickerState.selectedDateMillis?.let { millis ->
|
datePickerState.selectedDateMillis?.let { millis ->
|
||||||
val newDate = Instant.ofEpochMilli(millis)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
val newDate = Instant.fromEpochMilliseconds(millis)
|
||||||
.toLocalDate()
|
.toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||||
// Only allow present and future dates
|
// Only allow present and future dates
|
||||||
if (!newDate.isBefore(LocalDate.now())) {
|
if (newDate >= Clock.System.todayIn(TimeZone.currentSystemDefault())) {
|
||||||
onDateSelect(newDate)
|
onDateSelect(newDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,8 +146,8 @@ fun DatePicker(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class FutureSelectableDates : SelectableDates {
|
class FutureSelectableDates : SelectableDates {
|
||||||
private val now = LocalDate.now()
|
private val now = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
private val dayStart = now.atTime(0, 0, 0, 0).toEpochSecond(ZoneOffset.UTC) * 1000
|
private val dayStart = now.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds()
|
||||||
|
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
@ -159,8 +164,8 @@ class FutureSelectableDates : SelectableDates {
|
|||||||
@PreviewDevices
|
@PreviewDevices
|
||||||
@Composable
|
@Composable
|
||||||
private fun DatePickerPreview() {
|
private fun DatePickerPreview() {
|
||||||
val today = LocalDate.now()
|
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
val futureDate = today.plusDays(7) // A week from today
|
val futureDate = today.plus(7, DateTimeUnit.DAY)
|
||||||
|
|
||||||
FlightsTheme {
|
FlightsTheme {
|
||||||
DatePicker(
|
DatePicker(
|
||||||
|
@ -14,6 +14,9 @@ import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
|||||||
import dev.adriankuta.flights.ui.home.HomeUiState
|
import dev.adriankuta.flights.ui.home.HomeUiState
|
||||||
import dev.adriankuta.flights.ui.home.PassengersState
|
import dev.adriankuta.flights.ui.home.PassengersState
|
||||||
import dev.adriankuta.flights.ui.sharedui.Counter
|
import dev.adriankuta.flights.ui.sharedui.Counter
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PassengersOptions(
|
internal fun PassengersOptions(
|
||||||
@ -71,7 +74,7 @@ private fun PassengersOptionsPreview() {
|
|||||||
destinationAirports = emptyList(),
|
destinationAirports = emptyList(),
|
||||||
selectedOriginAirport = null,
|
selectedOriginAirport = null,
|
||||||
selectedDestinationAirport = null,
|
selectedDestinationAirport = null,
|
||||||
selectedDate = java.time.LocalDate.now(),
|
selectedDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
|
||||||
passengers = samplePassengersState,
|
passengers = samplePassengersState,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,7 +19,12 @@ import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
|||||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||||
import dev.adriankuta.flights.ui.home.HomeUiState
|
import dev.adriankuta.flights.ui.home.HomeUiState
|
||||||
import dev.adriankuta.flights.ui.home.PassengersState
|
import dev.adriankuta.flights.ui.home.PassengersState
|
||||||
import java.time.LocalDate
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.DateTimeUnit
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.plus
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SearchForm(
|
internal fun SearchForm(
|
||||||
@ -155,7 +160,8 @@ private fun SearchFormPreview() {
|
|||||||
destinationAirports = sampleAirports.drop(1),
|
destinationAirports = sampleAirports.drop(1),
|
||||||
selectedOriginAirport = sampleAirports.first(),
|
selectedOriginAirport = sampleAirports.first(),
|
||||||
selectedDestinationAirport = sampleAirports.last(),
|
selectedDestinationAirport = sampleAirports.last(),
|
||||||
selectedDate = LocalDate.now().plusDays(7),
|
selectedDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
|
.plus(7, DateTimeUnit.DAY),
|
||||||
passengers = samplePassengersState,
|
passengers = samplePassengersState,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,170 @@
|
|||||||
|
package dev.adriankuta.flights.ui.home.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
|
import dev.adriankuta.flights.domain.types.Trip
|
||||||
|
import dev.adriankuta.flights.domain.types.TripDate
|
||||||
|
import dev.adriankuta.flights.domain.types.TripFlight
|
||||||
|
import dev.adriankuta.flights.ui.home.SearchResultUiState
|
||||||
|
import dev.adriankuta.flights.ui.home.sample.FlightPreviewParameterProvider
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SearchResults(
|
||||||
|
uiState: SearchResultUiState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Text(
|
||||||
|
text = "Search Results",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (uiState) {
|
||||||
|
is SearchResultUiState.Error -> item { Text("Error") }
|
||||||
|
SearchResultUiState.Idle -> Unit
|
||||||
|
SearchResultUiState.Loading -> item { Text("Loading") }
|
||||||
|
is SearchResultUiState.Success -> items(uiState.flight.trips) { trip ->
|
||||||
|
TripCard(trip = trip, currency = uiState.flight.currency)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TripCard(
|
||||||
|
trip: Trip,
|
||||||
|
currency: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${trip.origin} → ${trip.destination}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
trip.dates.forEach { tripDate ->
|
||||||
|
TripDateItem(tripDate = tripDate, currency = currency)
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TripDateItem(
|
||||||
|
tripDate: TripDate,
|
||||||
|
currency: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = "Date: ${tripDate.dateOut}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
tripDate.flights.forEach { flight ->
|
||||||
|
FlightItem(flight = flight, currency = currency)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FlightItem(
|
||||||
|
flight: TripFlight,
|
||||||
|
currency: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Flight: ${flight.flightNumber}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Duration: ${flight.duration}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Operated by: ${flight.operatedBy}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Display price information
|
||||||
|
val totalPrice = flight.regularFare.fares.sumOf { it.amount * it.count }
|
||||||
|
Text(
|
||||||
|
text = "Price: $totalPrice $currency",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Seats available: ${flight.faresLeft}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun SearchResultsPreview(@PreviewParameter(FlightPreviewParameterProvider::class) flight: Flight) {
|
||||||
|
SearchResults(uiState = SearchResultUiState.Success(flight))
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
package dev.adriankuta.flights.ui.home.sample
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
|
import dev.adriankuta.flights.domain.types.RegularFare
|
||||||
|
import dev.adriankuta.flights.domain.types.Segment
|
||||||
|
import dev.adriankuta.flights.domain.types.Trip
|
||||||
|
import dev.adriankuta.flights.domain.types.TripDate
|
||||||
|
import dev.adriankuta.flights.domain.types.TripFare
|
||||||
|
import dev.adriankuta.flights.domain.types.TripFlight
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
|
internal class FlightPreviewParameterProvider : PreviewParameterProvider<Flight> {
|
||||||
|
override val values: Sequence<Flight> = sequenceOf(
|
||||||
|
Flight(
|
||||||
|
currency = "EUR",
|
||||||
|
currPrecision = 2,
|
||||||
|
trips = listOf(
|
||||||
|
Trip(
|
||||||
|
origin = "WAW",
|
||||||
|
destination = "LDN",
|
||||||
|
dates = listOf(
|
||||||
|
TripDate(
|
||||||
|
dateOut = LocalDate(2023, 6, 15),
|
||||||
|
flights = listOf(
|
||||||
|
TripFlight(
|
||||||
|
faresLeft = 5,
|
||||||
|
regularFare = RegularFare(
|
||||||
|
fares = listOf(
|
||||||
|
TripFare(
|
||||||
|
type = "ADULT",
|
||||||
|
amount = 99.99,
|
||||||
|
count = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flightNumber = "FR1234",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-15T10:00:00",
|
||||||
|
"2023-06-15T12:30:00",
|
||||||
|
),
|
||||||
|
duration = "2h 30m",
|
||||||
|
segments = listOf(
|
||||||
|
Segment(
|
||||||
|
origin = "WAW",
|
||||||
|
destination = "LDN",
|
||||||
|
flightNumber = "FR1234",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-15T10:00:00",
|
||||||
|
"2023-06-15T12:30:00",
|
||||||
|
),
|
||||||
|
duration = "2h 30m",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
operatedBy = "Ryanair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TripDate(
|
||||||
|
dateOut = LocalDate(2023, 6, 16),
|
||||||
|
flights = listOf(
|
||||||
|
TripFlight(
|
||||||
|
faresLeft = 3,
|
||||||
|
regularFare = RegularFare(
|
||||||
|
fares = listOf(
|
||||||
|
TripFare(
|
||||||
|
type = "ADULT",
|
||||||
|
amount = 129.99,
|
||||||
|
count = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flightNumber = "FR5678",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-16T14:00:00",
|
||||||
|
"2023-06-16T16:15:00",
|
||||||
|
),
|
||||||
|
duration = "2h 15m",
|
||||||
|
segments = listOf(
|
||||||
|
Segment(
|
||||||
|
origin = "WAW",
|
||||||
|
destination = "LDN",
|
||||||
|
flightNumber = "FR5678",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-16T14:00:00",
|
||||||
|
"2023-06-16T16:15:00",
|
||||||
|
),
|
||||||
|
duration = "2h 15m",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
operatedBy = "Ryanair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Trip(
|
||||||
|
origin = "LDN",
|
||||||
|
destination = "WAW",
|
||||||
|
dates = listOf(
|
||||||
|
TripDate(
|
||||||
|
dateOut = LocalDate(2023, 6, 22),
|
||||||
|
flights = listOf(
|
||||||
|
TripFlight(
|
||||||
|
faresLeft = 10,
|
||||||
|
regularFare = RegularFare(
|
||||||
|
fares = listOf(
|
||||||
|
TripFare(
|
||||||
|
type = "ADULT",
|
||||||
|
amount = 89.99,
|
||||||
|
count = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flightNumber = "FR9876",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-22T08:30:00",
|
||||||
|
"2023-06-22T10:45:00",
|
||||||
|
),
|
||||||
|
duration = "2h 15m",
|
||||||
|
segments = listOf(
|
||||||
|
Segment(
|
||||||
|
origin = "LDN",
|
||||||
|
destination = "WAW",
|
||||||
|
flightNumber = "FR9876",
|
||||||
|
dateTimes = listOf(
|
||||||
|
"2023-06-22T08:30:00",
|
||||||
|
"2023-06-22T10:45:00",
|
||||||
|
),
|
||||||
|
duration = "2h 15m",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
operatedBy = "Ryanair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package dev.adriankuta.flights.ui.home.sample
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import dev.adriankuta.flights.domain.types.City
|
||||||
|
import dev.adriankuta.flights.domain.types.Coordinates
|
||||||
|
import dev.adriankuta.flights.domain.types.Country
|
||||||
|
import dev.adriankuta.flights.domain.types.MacCity
|
||||||
|
import dev.adriankuta.flights.domain.types.Region
|
||||||
|
import dev.adriankuta.flights.ui.home.HomeUiState
|
||||||
|
import dev.adriankuta.flights.ui.home.PassengersState
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
|
|
||||||
|
internal class HomeUiStatePreviewParameterProvider : PreviewParameterProvider<HomeUiState> {
|
||||||
|
|
||||||
|
private val mockAirports = listOf(
|
||||||
|
AirportInfo(
|
||||||
|
code = "WAW",
|
||||||
|
name = "Warsaw Chopin Airport",
|
||||||
|
seoName = "warsaw",
|
||||||
|
isBase = true,
|
||||||
|
timeZone = "Europe/Warsaw",
|
||||||
|
city = City("WAW", "Warsaw"),
|
||||||
|
macCity = MacCity("WARSAW"),
|
||||||
|
region = Region("WARSAW_PL", "Warsaw"),
|
||||||
|
country = Country("PL", "Poland", "PLN"),
|
||||||
|
coordinates = Coordinates(52.1657, 20.9671),
|
||||||
|
),
|
||||||
|
AirportInfo(
|
||||||
|
code = "KRK",
|
||||||
|
name = "Krakow Airport",
|
||||||
|
seoName = "krakow",
|
||||||
|
isBase = true,
|
||||||
|
timeZone = "Europe/Warsaw",
|
||||||
|
city = City("KRK", "Krakow"),
|
||||||
|
macCity = MacCity("KRAKOW"),
|
||||||
|
region = Region("KRAKOW_PL", "Krakow"),
|
||||||
|
country = Country("PL", "Poland", "PLN"),
|
||||||
|
coordinates = Coordinates(50.0777, 19.7848),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
override val values: Sequence<HomeUiState> = sequenceOf(
|
||||||
|
HomeUiState.Success(
|
||||||
|
originAirports = mockAirports,
|
||||||
|
destinationAirports = mockAirports,
|
||||||
|
selectedOriginAirport = mockAirports.first(),
|
||||||
|
selectedDestinationAirport = mockAirports.last(),
|
||||||
|
selectedDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
|
||||||
|
passengers = PassengersState(
|
||||||
|
adultCount = 1,
|
||||||
|
teenCount = 0,
|
||||||
|
childCount = 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
Reference in New Issue
Block a user