-
Notifications
You must be signed in to change notification settings - Fork 0
Make ecosystem places searchable #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e7c1ecc
5acb9a3
8f357d9
44cd1af
6684f06
57bee17
cb5ff06
5937c21
f7a0558
ddf126c
747f80a
6098dae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package com.cornellappdev.transit.models.search | ||
|
|
||
| import com.cornellappdev.transit.models.Place | ||
| import com.cornellappdev.transit.models.RouteRepository | ||
| import com.cornellappdev.transit.models.ecosystem.EateryRepository | ||
| import com.cornellappdev.transit.models.ecosystem.GymRepository | ||
| import com.cornellappdev.transit.networking.ApiResponse | ||
| import com.cornellappdev.transit.util.ecosystem.buildEcosystemSearchPlaces | ||
| import com.cornellappdev.transit.util.ecosystem.mergeAndRankSearchResults | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.SharingStarted | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.combine | ||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.stateIn | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * Centralized search data source that merges route search results with ecosystem places. | ||
| * | ||
| * UI and ViewModels can share this data pipeline to keep search behavior consistent. | ||
| */ | ||
| @Singleton | ||
| class UnifiedSearchRepository @Inject constructor( | ||
| private val routeRepository: RouteRepository, | ||
| eateryRepository: EateryRepository, | ||
| gymRepository: GymRepository, | ||
| coroutineScope: CoroutineScope, | ||
| ) { | ||
|
|
||
| val ecosystemSearchPlacesFlow: StateFlow<List<Place>> = | ||
| combine( | ||
| routeRepository.printerFlow, | ||
| routeRepository.libraryFlow, | ||
| eateryRepository.eateryFlow, | ||
| gymRepository.gymFlow | ||
| ) { printers, libraries, eateries, gyms -> | ||
| buildEcosystemSearchPlaces( | ||
| printers = printers, | ||
| libraries = libraries, | ||
| eateries = eateries, | ||
| gyms = gyms | ||
| ) | ||
| }.stateIn( | ||
| scope = coroutineScope, | ||
| started = SharingStarted.Eagerly, | ||
| initialValue = emptyList() | ||
| ) | ||
|
|
||
| fun mergedSearchResults(queryFlow: Flow<String>): Flow<ApiResponse<List<Place>>> = | ||
| combine( | ||
| queryFlow | ||
| .map { it.trim() } | ||
| .distinctUntilChanged(), | ||
| routeRepository.placeSearchStateFlow, | ||
| ecosystemSearchPlacesFlow | ||
| ) { query, routeSearchState, ecosystemPlaces -> | ||
| val routeSearchResults = when { | ||
| query.isBlank() -> ApiResponse.Success(emptyList()) | ||
| routeSearchState.query.equals(query, ignoreCase = true) -> routeSearchState.response | ||
RyanCheung555 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else -> ApiResponse.Pending | ||
| } | ||
|
|
||
| mergeAndRankSearchResults( | ||
| query = query, | ||
| routeSearchResults = routeSearchResults, | ||
| ecosystemPlaces = ecosystemPlaces | ||
| ) | ||
| } | ||
RyanCheung555 marked this conversation as resolved.
Show resolved
Hide resolved
RyanCheung555 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,7 @@ import androidx.compose.foundation.clickable | |||||||||||||||||||
| import androidx.compose.foundation.layout.Column | ||||||||||||||||||||
| import androidx.compose.foundation.layout.Row | ||||||||||||||||||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||||||||||||||||||
| import androidx.compose.foundation.layout.padding | ||||||||||||||||||||
| import androidx.compose.foundation.layout.size | ||||||||||||||||||||
| import androidx.compose.material3.Text | ||||||||||||||||||||
| import androidx.compose.runtime.Composable | ||||||||||||||||||||
| import androidx.compose.ui.Alignment | ||||||||||||||||||||
|
|
@@ -15,6 +15,10 @@ import androidx.compose.ui.res.painterResource | |||||||||||||||||||
| import androidx.compose.ui.text.style.TextOverflow | ||||||||||||||||||||
| import androidx.compose.ui.tooling.preview.Preview | ||||||||||||||||||||
| import androidx.compose.ui.unit.dp | ||||||||||||||||||||
| import androidx.annotation.DrawableRes | ||||||||||||||||||||
| import androidx.compose.foundation.layout.Spacer | ||||||||||||||||||||
| import androidx.compose.foundation.layout.defaultMinSize | ||||||||||||||||||||
| import androidx.compose.foundation.layout.width | ||||||||||||||||||||
| import com.cornellappdev.transit.R | ||||||||||||||||||||
| import com.cornellappdev.transit.models.PlaceType | ||||||||||||||||||||
| import com.cornellappdev.transit.ui.theme.PrimaryText | ||||||||||||||||||||
|
|
@@ -23,33 +27,34 @@ import com.cornellappdev.transit.ui.theme.Style | |||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Card for each entry in the search bar | ||||||||||||||||||||
| * @param icon The icon for the item | ||||||||||||||||||||
| * @param type The place type used to resolve icon | ||||||||||||||||||||
| * @param label The label for the item | ||||||||||||||||||||
| * @param sublabel The sublabel for the item | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Unit) { | ||||||||||||||||||||
| fun MenuItem( | ||||||||||||||||||||
| type: PlaceType, | ||||||||||||||||||||
| label: String, | ||||||||||||||||||||
| sublabel: String, | ||||||||||||||||||||
| onClick: () -> Unit, | ||||||||||||||||||||
| modifier: Modifier = Modifier | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| val displayedSublabel = ecosystemSublabelFor(type) ?: sublabel | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Row( | ||||||||||||||||||||
| Modifier | ||||||||||||||||||||
| modifier | ||||||||||||||||||||
| .fillMaxWidth() | ||||||||||||||||||||
| .padding(horizontal = 16.dp, vertical = 6.dp) | ||||||||||||||||||||
| .defaultMinSize(minHeight = 36.dp) | ||||||||||||||||||||
| .clickable(onClick = onClick), | ||||||||||||||||||||
|
Comment on lines
+45
to
48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Increase clickable height to meet accessibility touch targets. Line 46 sets a 36.dp minimum height on a clickable row, which can result in a tap target below the 48.dp baseline. ✅ Suggested fix- .defaultMinSize(minHeight = 36.dp)
+ .defaultMinSize(minHeight = 48.dp)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this important? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why it matters:
Why it might be acceptable for now:
Recommendation: It's worth fixing — changing |
||||||||||||||||||||
| verticalAlignment = Alignment.CenterVertically | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| //TODO: Add icons for each ecosystem type | ||||||||||||||||||||
| if (type == PlaceType.APPLE_PLACE) { | ||||||||||||||||||||
| Image( | ||||||||||||||||||||
| painterResource(R.drawable.location_pin_gray), | ||||||||||||||||||||
| contentDescription = "Place", | ||||||||||||||||||||
| modifier = Modifier.padding(end = 20.dp), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Image( | ||||||||||||||||||||
| painterResource(R.drawable.bus_stop_pin), | ||||||||||||||||||||
| contentDescription = "Stop", | ||||||||||||||||||||
| modifier = Modifier.padding(end = 20.dp), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Image( | ||||||||||||||||||||
| painterResource(iconForPlaceType(type)), | ||||||||||||||||||||
| contentDescription = null, | ||||||||||||||||||||
| modifier = Modifier | ||||||||||||||||||||
| .size(24.dp), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| Spacer(modifier = Modifier.width(12.dp)) | ||||||||||||||||||||
| Column() { | ||||||||||||||||||||
| Text( | ||||||||||||||||||||
| text = label, | ||||||||||||||||||||
|
|
@@ -59,7 +64,7 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un | |||||||||||||||||||
| style = Style.heading3 | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| Text( | ||||||||||||||||||||
| text = sublabel, | ||||||||||||||||||||
| text = displayedSublabel, | ||||||||||||||||||||
| color = SecondaryText, | ||||||||||||||||||||
| maxLines = 1, | ||||||||||||||||||||
| overflow = TextOverflow.Ellipsis, | ||||||||||||||||||||
|
|
@@ -69,6 +74,27 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private fun ecosystemSublabelFor(type: PlaceType): String? = when (type) { | ||||||||||||||||||||
| PlaceType.EATERY -> "Eatery" | ||||||||||||||||||||
| PlaceType.LIBRARY -> "Library" | ||||||||||||||||||||
| PlaceType.PRINTER -> "Printer" | ||||||||||||||||||||
| PlaceType.GYM -> "Gym" | ||||||||||||||||||||
| else -> null | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private val searchIconByPlaceType = mapOf( | ||||||||||||||||||||
| PlaceType.BUS_STOP to R.drawable.bus_stop_pin, | ||||||||||||||||||||
| PlaceType.APPLE_PLACE to R.drawable.location_pin_gray, | ||||||||||||||||||||
| PlaceType.EATERY to R.drawable.eatery_icon, | ||||||||||||||||||||
| PlaceType.LIBRARY to R.drawable.library_icon, | ||||||||||||||||||||
| PlaceType.GYM to R.drawable.gym_icon, | ||||||||||||||||||||
| PlaceType.PRINTER to R.drawable.printer_icon | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @DrawableRes | ||||||||||||||||||||
| private fun iconForPlaceType(type: PlaceType): Int { | ||||||||||||||||||||
| return searchIconByPlaceType[type] ?: R.drawable.location_pin_gray | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Preview(showBackground = true) | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
|
|
@@ -80,4 +106,28 @@ private fun PreviewMenuItemBusStop() { | |||||||||||||||||||
| @Composable | ||||||||||||||||||||
| private fun PreviewMenuItemApplePlace() { | ||||||||||||||||||||
| MenuItem(PlaceType.APPLE_PLACE, "Apple Place", "Ithaca, NY", {}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Preview(showBackground = true) | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| private fun PreviewMenuItemEatery() { | ||||||||||||||||||||
| MenuItem(PlaceType.EATERY, "Eatery", "Ithaca, NY", {}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Preview(showBackground = true) | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| private fun PreviewMenuItemGym() { | ||||||||||||||||||||
| MenuItem(PlaceType.GYM, "Gym", "Ithaca, NY", {}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Preview(showBackground = true) | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| private fun PreviewMenuItemLibrary() { | ||||||||||||||||||||
| MenuItem(PlaceType.LIBRARY, "Library", "Ithaca, NY", {}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Preview(showBackground = true) | ||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| private fun PreviewMenuItemPrinter() { | ||||||||||||||||||||
| MenuItem(PlaceType.PRINTER, "Printer", "Ithaca, NY", {}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.