From 0b118e75ab2dd76514cf1c5809e50fd9a5aef4af Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Mon, 10 Nov 2025 15:16:06 -0500 Subject: [PATCH 1/6] WIP generalize ecosystem UI to eatery --- app/build.gradle.kts | 4 ++ .../home/EcosystemBottomSheetContent.kt | 28 ++++++-- .../components/home/RoundedImagePlaceCard.kt | 68 +++++++++++++------ .../transit/util/ecosystem/PlaceUtils.kt | 12 ++++ 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9b8d9e..d038517 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + // Coil images + implementation("io.coil-kt.coil3:coil-compose:3.0.4") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4") + //Maps implementation("com.google.maps.android:maps-compose:4.0.0") implementation("com.google.android.gms:play-services-maps:19.0.0") diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt index 7f86542..0281ebb 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.cornellappdev.transit.R import com.cornellappdev.transit.models.Place -import com.cornellappdev.transit.models.PlaceType import com.cornellappdev.transit.models.ecosystem.DetailedEcosystemPlace import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.networking.ApiResponse @@ -119,7 +118,13 @@ private fun BottomSheetFilteredContent( } FilterState.EATERIES -> { - eateryList(staticPlaces, navigateToPlace) + eateryList( + staticPlaces, + navigateToPlace, + onDetailsClick, + favorites, + onFavoriteStarClick, + ) } FilterState.LIBRARIES -> { @@ -213,7 +218,10 @@ private fun LazyListScope.printerList( */ private fun LazyListScope.eateryList( staticPlaces: StaticPlaces, - navigateToPlace: (Place) -> Unit + navigateToPlace: (Place) -> Unit, + navigateToDetails: (DetailedEcosystemPlace) -> Unit, + favorites: Set, + onFavoriteStarClick: (Place) -> Unit ) { when (staticPlaces.eateries) { is ApiResponse.Error -> { @@ -224,11 +232,17 @@ private fun LazyListScope.eateryList( is ApiResponse.Success -> { items(staticPlaces.eateries.data) { - BottomSheetLocationCard( + RoundedImagePlaceCard( + imageUrl = it.imageUrl, title = it.name, - subtitle1 = it.location.orEmpty() + subtitle = it.location ?: "", + isFavorite = it.toPlace() in favorites, + onFavoriteClick = { + onFavoriteStarClick(it.toPlace()) + }, + placeholderRes = R.drawable.olin_library, ) { - //TODO: Eatery + navigateToDetails(it) } } } @@ -255,7 +269,7 @@ private fun LazyListScope.libraryList( is ApiResponse.Success -> { items(staticPlaces.libraries.data) { RoundedImagePlaceCard( - imageRes = R.drawable.olin_library, + placeholderRes = R.drawable.olin_library, title = it.location, subtitle = it.address, isFavorite = it.toPlace() in favorites, diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt index c58f95c..ef04963 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt @@ -1,5 +1,6 @@ package com.cornellappdev.transit.ui.components.home +import android.util.Log import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -12,45 +13,43 @@ 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.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.outlined.Star -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.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.PlatformTextStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade import com.cornellappdev.transit.R -import com.cornellappdev.transit.ui.theme.FavoritesYellow import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style +/** + * Card for a place with a rounded image on top + */ @Composable fun RoundedImagePlaceCard( - @DrawableRes imageRes: Int, + imageUrl: String? = null, title: String, subtitle: String, isFavorite: Boolean, onFavoriteClick: () -> Unit, leftAnnotatedString: AnnotatedString? = null, rightAnnotatedString: AnnotatedString? = null, - onClick: () -> Unit + @DrawableRes placeholderRes: Int, + onClick: () -> Unit, ) { Column( modifier = Modifier @@ -62,15 +61,7 @@ fun RoundedImagePlaceCard( .fillMaxWidth() .background(Color.White, shape = RoundedCornerShape(12.dp)) ) { - Image( - painter = painterResource(id = imageRes), - contentDescription = title, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(112.dp) - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - ) + PlaceCardImage(imageUrl, placeholderRes) Box( modifier = Modifier @@ -124,11 +115,44 @@ fun RoundedImagePlaceCard( } } +@Composable +fun PlaceCardImage(imageUrl: String?, @DrawableRes placeholderRes: Int) { + val imageModifier = Modifier + .fillMaxWidth() + .height(112.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + + if (imageUrl.isNullOrBlank()) { + Image( + painter = painterResource(id = placeholderRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } else { + Log.d("RoundedImagePlaceCard", imageUrl.toString()) + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + placeholder = painterResource(placeholderRes), + error = painterResource(placeholderRes), + onError = { + Log.d("RoundedImagePlaceCard", it.toString()) + }, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } +} + @Preview @Composable fun RoundedImagePlaceCardPreview() { RoundedImagePlaceCard( - imageRes = R.drawable.olin_library, + placeholderRes = R.drawable.olin_library, title = "Olin Library", subtitle = "Ho Plaza", isFavorite = true, diff --git a/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt index da842a1..79a75e2 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt @@ -2,6 +2,7 @@ package com.cornellappdev.transit.util.ecosystem import com.cornellappdev.transit.models.Place import com.cornellappdev.transit.models.PlaceType +import com.cornellappdev.transit.models.ecosystem.Eatery import com.cornellappdev.transit.models.ecosystem.Library import com.cornellappdev.transit.models.ecosystem.Printer @@ -27,3 +28,14 @@ fun Printer.toPlace(): Place = Place( detail = this.description, type = PlaceType.APPLE_PLACE ) + +/** + * Predefined mapping from eatery to generic place. Nullable latitudes and longitudes default to 0 + */ +fun Eatery.toPlace(): Place = Place( + latitude = this.latitude ?: 0.0, + longitude = this.longitude ?: 0.0, + name = this.name, + detail = this.location, + type = PlaceType.APPLE_PLACE +) From 328f91f01f0d96c57bc05eabda638eb511badefd Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Tue, 18 Nov 2025 15:52:33 -0500 Subject: [PATCH 2/6] Eatery end to end ecosystem integration --- app/build.gradle.kts | 3 +- .../transit/TransitApplication.kt | 21 ++- .../transit/models/ecosystem/Eatery.kt | 82 ++++++++- .../models/ecosystem/OperatingHours.kt | 3 + .../transit/networking/EateryNetworkApi.kt | 2 +- .../transit/networking/MoshiAdapters.kt | 24 +++ .../transit/networking/NetworkModule.kt | 1 + .../home/CenteredSpinningIndicator.kt | 26 +++ .../home/DetailedPlaceHeaderSection.kt | 82 +++++++++ .../home/DetailedPlaceSheetContent.kt | 13 +- .../components/home/EateryDetailsContent.kt | 129 +++++++++++++ .../home/EcosystemBottomSheetContent.kt | 29 ++- .../ui/components/home/OperatingHoursList.kt | 124 +++++++++++++ .../ui/components/home/PlaceCardImage.kt | 49 +++++ .../components/home/RoundedImagePlaceCard.kt | 49 +---- .../transit/ui/screens/RouteScreen.kt | 1 + .../cornellappdev/transit/ui/theme/Style.kt | 19 ++ .../transit/ui/viewmodels/HomeViewModel.kt | 170 ++++++++++++++++++ .../transit/ui/viewmodels/OpenStatus.kt | 3 + .../transit/util/ContentConstants.kt | 22 +++ app/src/main/res/values/strings.xml | 1 + 21 files changed, 792 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d038517..69f0bd7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,9 +99,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - // Coil images + // Images implementation("io.coil-kt.coil3:coil-compose:3.0.4") implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4") + implementation("com.valentinilk.shimmer:compose-shimmer:1.3.1") //Maps implementation("com.google.maps.android:maps-compose:4.0.0") diff --git a/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt b/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt index 98e081a..4b7f5b5 100644 --- a/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt +++ b/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt @@ -1,7 +1,26 @@ package com.cornellappdev.transit import android.app.Application +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import coil3.request.crossfade import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class TransitApplication : Application() \ No newline at end of file +class TransitApplication : Application(), SingletonImageLoader.Factory { + override fun newImageLoader(context: PlatformContext): ImageLoader { + // Default image loading procedure for network request images + return ImageLoader.Builder(context) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, 0.25) + .build() + } + .crossfade(true) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt index 104e0bf..f2a88e2 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt @@ -1,10 +1,15 @@ package com.cornellappdev.transit.models.ecosystem import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter /** * Data class representing an eatery */ +@JsonClass(generateAdapter = true) data class Eatery( @Json(name = "id") var id: Int, @Json(name = "name") var name: String, @@ -18,4 +23,79 @@ data class Eatery( @Json(name = "payment_accepts_meal_swipes") var paymentAcceptsMealSwipes: Boolean?, @Json(name = "payment_accepts_brbs") var paymentAcceptsBrbs: Boolean?, @Json(name = "payment_accepts_cash") var paymentAcceptsCash: Boolean?, - ) : DetailedEcosystemPlace \ No newline at end of file + @Json(name = "events") val events: List? +) : DetailedEcosystemPlace { + + /** + * Value to represent the custom order of days in a week (with Sunday as + * the first day due to a particular design choice). Used for sorting purposes + */ + private val dayOrder = mapOf( + "Sunday" to 1, + "Monday" to 2, + "Tuesday" to 3, + "Wednesday" to 4, + "Thursday" to 5, + "Friday" to 6, + "Saturday" to 7 + ) + + /** + * @Return a list of pairs representing each day of the week + * and the corresponding times that an eatery is open. The list is sorted + * by day with the custom dayOrder (Sunday first). + */ + fun formatOperatingHours(): OperatingHours { + val dailyHours = operatingHours() + + // Convert map to list and sort by custom day order + return dailyHours.entries + .sortedBy { entry -> + val dayName = + entry.key.name.take(1).uppercase() + entry.key.name.drop(1).lowercase() + dayOrder[dayName] ?: Int.MAX_VALUE + } + .map { entry -> + val dayName = + entry.key.name.take(1).uppercase() + entry.key.name.drop(1).lowercase() + dayName to entry.value + } + } + + /** + * @Return a map of each day of the week to its list of operating hours + */ + private fun operatingHours(): Map> { + val dailyHours = mutableMapOf>() + + events?.forEach { event -> + val dayOfWeek = event.startTime?.dayOfWeek + val openTime = event.startTime?.format(DateTimeFormatter.ofPattern("h:mm a")) + val closeTime = event.endTime?.format(DateTimeFormatter.ofPattern("h:mm a")) + + val timeString = "$openTime - $closeTime" + + if (dayOfWeek != null && dailyHours[dayOfWeek]?.none { it.contains(timeString) } != false) { + dailyHours.computeIfAbsent(dayOfWeek) { mutableListOf() }.add(timeString) + } + } + + DayOfWeek.values().forEach { dayOfWeek -> + dailyHours.computeIfAbsent(dayOfWeek) { mutableListOf("Closed") } + } + + return dailyHours + } +} + + +@JsonClass(generateAdapter = true) +data class Event( + @Json(name = "id") val id: Int? = null, + /** + * Descriptions tend to be "Lunch", "Dinner", etc.. + */ + @Json(name = "event_description") val description: String? = null, + @Json(name = "start") val startTime: LocalDateTime? = null, + @Json(name = "end") val endTime: LocalDateTime? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt new file mode 100644 index 0000000..6a7bdb8 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt @@ -0,0 +1,3 @@ +package com.cornellappdev.transit.models.ecosystem + +typealias OperatingHours = List>> diff --git a/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt b/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt index a64af5c..77c7b0b 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt @@ -4,7 +4,7 @@ import com.cornellappdev.transit.models.ecosystem.Eatery import retrofit2.http.GET interface EateryNetworkApi { - @GET("/eatery/simple") + @GET("/eatery/") suspend fun getEateries(): List } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt b/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt index 639bafa..0a988e4 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt @@ -3,6 +3,12 @@ package com.cornellappdev.transit.networking import com.google.android.gms.maps.model.LatLng import com.squareup.moshi.FromJson import com.squareup.moshi.Json +import com.squareup.moshi.ToJson +import java.text.ParseException +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter class LatLngAdapter { @@ -21,4 +27,22 @@ class LatLngAdapter { fun fromJson(coord: Coordinate): LatLng { return LatLng(coord.latitude, coord.longitude) } +} + +class DateTimeAdapter { + @ToJson + fun toJson(dateTime: LocalDateTime): String { + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) + } + + @FromJson + fun fromJson(dateTime: Long): LocalDateTime { + try { + val instant = Instant.ofEpochSecond(dateTime) + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) + } catch (e: ParseException) { + e.printStackTrace() + } + return LocalDateTime.MIN + } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt b/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt index fc371d9..eae3f94 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt @@ -55,6 +55,7 @@ object NetworkModule { fun provideMoshi(): Moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add(LatLngAdapter()) + .add(DateTimeAdapter()) .build() @Provides diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt new file mode 100644 index 0000000..6b0bc3e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt @@ -0,0 +1,26 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.ui.theme.MetadataGray + +@Composable +fun CenteredSpinningIndicator() { + Row( + modifier = Modifier + .fillMaxWidth() + .height(138.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator(color = MetadataGray, modifier = Modifier.size(40.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt new file mode 100644 index 0000000..ae2da23 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt @@ -0,0 +1,82 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.ui.theme.PrimaryText +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style + +/** + * Text area of detailed place header with favorites star + */ +@Composable +fun DetailedPlaceHeaderSection( + title: String, + subtitle: String?, + leftAnnotatedString: AnnotatedString? = null, + rightAnnotatedString: AnnotatedString? = null, + onFavoriteClick: () -> Unit, + isFavorite: Boolean +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + ) { + Column( + modifier = Modifier + .align(Alignment.CenterStart), + ) { + Text( + text = title, + style = Style.detailHeading, + color = PrimaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp, bottom = 12.dp) + ) + subtitle?.let { + Text( + text = subtitle, + style = Style.cardSubtitle, + color = SecondaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp, bottom = 8.dp) + + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + leftAnnotatedString?.let { + Text( + text = leftAnnotatedString, + style = Style.cardSubtitle + ) + } + Spacer(modifier = Modifier.weight(1f)) + rightAnnotatedString?.let { + Text( + text = rightAnnotatedString, + style = Style.cardSubtitle + ) + } + } + } + + FavoritesStar(onFavoriteClick, isFavorite) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt index f1f8ae2..6fca4bc 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt @@ -90,8 +90,13 @@ fun DetailedPlaceSheetContent( // Main Content when (ecosystemPlace) { is Eatery -> { - //TODO - Text(ecosystemPlace.name) + EateryDetailsContent( + eatery = ecosystemPlace, + isFavorite = ecosystemPlace.toPlace() in favorites, + onFavoriteClick = { + onFavoriteStarClick(ecosystemPlace.toPlace()) + } + ) } is Library -> { @@ -131,7 +136,9 @@ fun DetailedPlaceSheetContent( .clickable { when (ecosystemPlace) { is Eatery -> { - //TODO + navigateToPlace( + ecosystemPlace.toPlace() + ) } is Library -> { diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt new file mode 100644 index 0000000..0c512dd --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt @@ -0,0 +1,129 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.transit.R +import com.cornellappdev.transit.models.ecosystem.Eatery +import com.cornellappdev.transit.ui.theme.DividerGray +import com.cornellappdev.transit.ui.theme.Gray05 +import com.cornellappdev.transit.ui.theme.PrimaryText +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style +import com.cornellappdev.transit.ui.theme.TransitBlue +import com.cornellappdev.transit.ui.viewmodels.HomeViewModel +import com.cornellappdev.transit.util.getAboutContent + +@Composable +fun EateryDetailsContent( + homeViewModel: HomeViewModel = hiltViewModel(), + eatery: Eatery, + isFavorite: Boolean, + onFavoriteClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 20.dp, + end = 20.dp, + ) + ) { + PlaceCardImage( + imageUrl = eatery.imageUrl, + placeholderRes = R.drawable.olin_library, + shouldClipBottom = true + ) + + DetailedPlaceHeaderSection( + eatery.name, + eatery.campusArea, + leftAnnotatedString = homeViewModel.isOpenAnnotatedStringFromOperatingHours( + eatery.formatOperatingHours() + ), + onFavoriteClick = onFavoriteClick, + isFavorite = isFavorite + ) + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "About", + style = Style.detailSubtitle, + color = PrimaryText, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Text( + text = getAboutContent(eatery.name), + style = Style.detailBody, + color = SecondaryText, + modifier = Modifier.padding(bottom = 15.dp) + ) + + Text( + text = stringResource(R.string.view_menu), + style = Style.heading2, + color = TransitBlue + ) + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + Spacer(modifier = Modifier.height(24.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(R.drawable.location_pin_gray), + contentDescription = null, + modifier = Modifier + .size(20.dp), + tint = Gray05 + ) + Text( + text = eatery.location ?: "", + style = Style.detailBody, + color = SecondaryText, + modifier = Modifier.padding(start = 15.dp) + ) + + } + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + Spacer(modifier = Modifier.height(24.dp)) + + OperatingHoursList(homeViewModel.rotateOperatingHours(eatery.formatOperatingHours())) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt index 0281ebb..69e3b11 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt @@ -14,17 +14,21 @@ 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.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.transit.R import com.cornellappdev.transit.models.Place import com.cornellappdev.transit.models.ecosystem.DetailedEcosystemPlace +import com.cornellappdev.transit.models.ecosystem.OperatingHours import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.ui.theme.robotoFamily import com.cornellappdev.transit.ui.viewmodels.FilterState +import com.cornellappdev.transit.ui.viewmodels.HomeViewModel import com.cornellappdev.transit.util.ecosystem.toPlace @@ -93,6 +97,7 @@ fun EcosystemBottomSheetContent( @Composable private fun BottomSheetFilteredContent( + homeViewModel: HomeViewModel = hiltViewModel(), currentFilter: FilterState, staticPlaces: StaticPlaces, favorites: Set, @@ -119,11 +124,11 @@ private fun BottomSheetFilteredContent( FilterState.EATERIES -> { eateryList( - staticPlaces, - navigateToPlace, - onDetailsClick, - favorites, - onFavoriteStarClick, + staticPlaces = staticPlaces, + onDetailsClick = onDetailsClick, + favorites = favorites, + onFavoriteStarClick = onFavoriteStarClick, + operatingHoursToString = homeViewModel::isOpenAnnotatedStringFromOperatingHours ) } @@ -218,16 +223,19 @@ private fun LazyListScope.printerList( */ private fun LazyListScope.eateryList( staticPlaces: StaticPlaces, - navigateToPlace: (Place) -> Unit, - navigateToDetails: (DetailedEcosystemPlace) -> Unit, + onDetailsClick: (DetailedEcosystemPlace) -> Unit, favorites: Set, - onFavoriteStarClick: (Place) -> Unit + onFavoriteStarClick: (Place) -> Unit, + operatingHoursToString: (OperatingHours) -> AnnotatedString ) { when (staticPlaces.eateries) { is ApiResponse.Error -> { } is ApiResponse.Pending -> { + item { + CenteredSpinningIndicator() + } } is ApiResponse.Success -> { @@ -241,8 +249,11 @@ private fun LazyListScope.eateryList( onFavoriteStarClick(it.toPlace()) }, placeholderRes = R.drawable.olin_library, + leftAnnotatedString = operatingHoursToString( + it.formatOperatingHours() + ) ) { - navigateToDetails(it) + onDetailsClick(it) } } } diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt new file mode 100644 index 0000000..0af8b2f --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt @@ -0,0 +1,124 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.models.ecosystem.OperatingHours +import com.cornellappdev.transit.ui.theme.PrimaryText +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style.heading3 +import com.cornellappdev.transit.ui.theme.Style.heading3Semibold + +/** + * Composable that displays operating hours for an eatery + * + * @param operatingHours List of pairs where first is the day name and second is list of time ranges + * @param modifier Modifier for the component + */ +@Composable +fun OperatingHoursList( + operatingHours: OperatingHours, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + operatingHours.forEachIndexed { index, (day, hours) -> + OperatingHoursRow( + day = day, + hours = hours, + index == 0 + ) + } + } +} + +/** + * Single row displaying a day and its operating hours + */ +@Composable +private fun OperatingHoursRow( + day: String, + hours: List, + isHighlighted: Boolean = false +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Row (horizontalArrangement = Arrangement.Start, + modifier = Modifier.weight(0.6f)) { + if (isHighlighted) { + VerticalDivider(color = PrimaryText, thickness = 2.dp, modifier = Modifier.height(16.dp)) + Spacer(Modifier.width(12.dp)) + } else { + Spacer(Modifier.width(14.dp)) + } + + Text( + text = day, + style = if (isHighlighted) heading3Semibold else heading3, + color = if (isHighlighted) PrimaryText else SecondaryText, + ) + } + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.weight(0.4f) + ) { + hours.forEach { timeRange -> + Text( + text = timeRange, + style = heading3, + color = SecondaryText, + modifier = Modifier.padding(bottom = 8.dp), + textAlign = TextAlign.Left + ) + } + } + } +} + +@Preview +@Composable +private fun OperatingHoursPreview() { + val sampleHours = listOf( + "Tuesday" to listOf("10:00 AM - 10:00 PM"), + "Wednesday" to listOf("10:00 AM - 5:00 PM"), + "Thursday" to listOf("Closed"), + "Friday" to listOf("10:00 AM - 5:00 PM"), + "Saturday" to listOf( + "8:00 AM - 9:30 AM", + "10:30 AM - 2:00 PM", + "5:00 PM - 8:00 PM" + ), + "Sunday" to listOf( + "10:00 AM - 2:00 PM", + "5:00 PM - 8:30 PM" + ), + "Monday" to listOf("10:00 AM - 10:00 PM") + ) + + OperatingHoursList(operatingHours = sampleHours) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt new file mode 100644 index 0000000..814205e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt @@ -0,0 +1,49 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.cornellappdev.transit.ui.theme.MetadataGray + +/** + * Rounded image from a network request, fallback to a drawable + */ +@Composable +fun PlaceCardImage(imageUrl: String?, @DrawableRes placeholderRes: Int, shouldClipBottom: Boolean = false) { + + val imageModifier = Modifier + .then( + if(shouldClipBottom) Modifier.clip(RoundedCornerShape(12.dp)) + else Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))) + .fillMaxWidth() + .height(112.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .background(MetadataGray) + + if (imageUrl.isNullOrBlank()) { + Image( + painter = painterResource(id = placeholderRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } else { + AsyncImage( + model = imageUrl, + contentDescription = null, + error = painterResource(id = placeholderRes), + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt index ef04963..9fa4ab0 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt @@ -1,8 +1,6 @@ package com.cornellappdev.transit.ui.components.home -import android.util.Log import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -11,26 +9,18 @@ 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.shape.RoundedCornerShape import androidx.compose.material3.Text 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.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import coil3.request.ImageRequest -import coil3.request.crossfade import com.cornellappdev.transit.R import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText @@ -96,13 +86,15 @@ fun RoundedImagePlaceCard( ) { leftAnnotatedString?.let { Text( - text = leftAnnotatedString + text = leftAnnotatedString, + style = Style.heading3 ) } Spacer(modifier = Modifier.weight(1f)) rightAnnotatedString?.let { Text( - text = rightAnnotatedString + text = rightAnnotatedString, + style = Style.heading3 ) } } @@ -115,39 +107,6 @@ fun RoundedImagePlaceCard( } } -@Composable -fun PlaceCardImage(imageUrl: String?, @DrawableRes placeholderRes: Int) { - val imageModifier = Modifier - .fillMaxWidth() - .height(112.dp) - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - - if (imageUrl.isNullOrBlank()) { - Image( - painter = painterResource(id = placeholderRes), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = imageModifier - ) - } else { - Log.d("RoundedImagePlaceCard", imageUrl.toString()) - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .crossfade(true) - .build(), - placeholder = painterResource(placeholderRes), - error = painterResource(placeholderRes), - onError = { - Log.d("RoundedImagePlaceCard", it.toString()) - }, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = imageModifier - ) - } -} - @Preview @Composable fun RoundedImagePlaceCardPreview() { diff --git a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt index c6d8716..07a1eff 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.cornellappdev.transit.R import com.cornellappdev.transit.models.MapState diff --git a/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt b/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt index 5ca2412..f7c0b77 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt @@ -55,6 +55,15 @@ object Style { ), ) + val cardSubtitle = TextStyle( + fontSize = 16.sp, + fontFamily = robotoFamily, + lineHeight = 16.sp, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + ) + val heading3Emphasized = TextStyle( fontSize = 14.sp, fontFamily = robotoFamily, @@ -65,6 +74,16 @@ object Style { fontWeight = FontWeight(500) ) + val heading3Semibold = TextStyle( + fontSize = 14.sp, + fontFamily = robotoFamily, + lineHeight = 16.sp, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + fontWeight = FontWeight(600) + ) + /** * TextStyle for headers of route types */ diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt index bd7c24d..a4c8b0a 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt @@ -1,6 +1,11 @@ package com.cornellappdev.transit.ui.viewmodels import android.content.Context +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.cornellappdev.transit.models.LocationRepository @@ -11,7 +16,12 @@ import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.models.UserPreferenceRepository import com.cornellappdev.transit.models.ecosystem.EateryRepository import com.cornellappdev.transit.models.ecosystem.GymRepository +import com.cornellappdev.transit.models.ecosystem.OperatingHours import com.cornellappdev.transit.networking.ApiResponse +import com.cornellappdev.transit.ui.theme.LateRed +import com.cornellappdev.transit.ui.theme.LiveGreen +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style.cardSubtitle import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -27,6 +37,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -274,4 +289,159 @@ class HomeViewModel @Inject constructor( this.filterState.value = filterState } + /** + * Rotate operating hours such that first value is today's date + * + * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open + */ + fun rotateOperatingHours( + operatingHours: OperatingHours, + currentDate: LocalDate = LocalDate.now() + ): OperatingHours { + val today = currentDate.dayOfWeek.name.lowercase() + .replaceFirstChar { it.uppercase() } + + val todayIndex = operatingHours.indexOfFirst { + it.first.equals(today, ignoreCase = true) + } + + // Defensive programming only if [operatingHours] is missing a day + if (todayIndex == -1) return operatingHours + + return operatingHours.drop(todayIndex) + operatingHours.take(todayIndex) + } + + /** + * Find the next time a place is open if it is closed for the day + */ + private fun findOpenNextDay(operatingHours: OperatingHours): OpenStatus { + // Check day after + val dayAfter = operatingHours[1].second + if (!dayAfter.any { it.equals("Closed", ignoreCase = true) }) { + val firstOpenTime = parseTimeRange(dayAfter[0])?.first + if (firstOpenTime != null) { + return OpenStatus( + false, + "until ${formatTime(firstOpenTime)}" + ) + } + } + // Find next open day + for (i in 2 until operatingHours.size) { + val currDay = operatingHours[i].second + if (!currDay.any { it.equals("Closed", ignoreCase = true) }) { + val dayName = operatingHours[i].first + return OpenStatus( + false, + "until $dayName" + ) + } + } + return OpenStatus(false, "Closed today") + } + + /** + * Given operating hours rotated for today's date, return whether it is open and when it is open until + * or when it will next open + * + * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open + */ + private fun getOpenStatus( + operatingHours: OperatingHours, + currentDateTime: LocalDateTime = LocalDateTime.now() + ): OpenStatus { + + val currentTime = currentDateTime.toLocalTime() + val todaySchedule = operatingHours[0].second // First day should be today after rotation + + // Check if closed today + if (todaySchedule.any { it.equals("Closed", ignoreCase = true) }) { + return findOpenNextDay(operatingHours) + } + + val timeRanges = todaySchedule.mapNotNull { parseTimeRange(it) } + + // Check if currently open + for (range in timeRanges) { + if (currentTime >= range.first && currentTime < range.second) { + return OpenStatus(true, "until ${formatTime(range.second)}") + } + } + + // Check if opens later today + for (range in timeRanges) { + if (currentTime < range.first) { + return OpenStatus(false, "until ${formatTime(range.first)}") + } + } + + // Closed for today, find next open day + return findOpenNextDay(operatingHours) + } + + /** + * Return annotated string for open times + */ + private fun getOpenStatusAnnotatedString(openStatus: OpenStatus): AnnotatedString { + return buildAnnotatedString { + if (openStatus.isOpen) { + withStyle( + style = SpanStyle( + color = LiveGreen, + ) + ) { + append("Open") + } + } else { + withStyle( + style = SpanStyle( + color = LateRed + ) + ) { + append("Closed") + } + } + withStyle( + style = SpanStyle( + color = SecondaryText + ) + ) { + append(" - ") + append(openStatus.nextChangeTime) + } + } + } + + private fun parseTimeRange(timeString: String): Pair? { + if (timeString.equals("Closed", ignoreCase = true)) return null + + val parts = timeString.split("-").map { it.trim() } + if (parts.size != 2) return null + + return try { + val formatter = DateTimeFormatter.ofPattern("h:mm a") + val start = LocalTime.parse(parts[0], formatter) + val end = LocalTime.parse(parts[1], formatter) + start to end + } catch (e: Exception) { + null + } + } + + private fun formatTime(time: LocalTime): String { + val formatter = DateTimeFormatter.ofPattern("h:mm a") + return time.format(formatter) + } + + /** + * Rotate operating hours to current day, then determine if place is open, then format string + */ + fun isOpenAnnotatedStringFromOperatingHours(operatingHours: OperatingHours): AnnotatedString { + return getOpenStatusAnnotatedString( + getOpenStatus( + rotateOperatingHours(operatingHours) + ) + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt new file mode 100644 index 0000000..93171c9 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt @@ -0,0 +1,3 @@ +package com.cornellappdev.transit.ui.viewmodels + +data class OpenStatus(val isOpen: Boolean, val nextChangeTime: String) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt new file mode 100644 index 0000000..d43f21d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt @@ -0,0 +1,22 @@ +package com.cornellappdev.transit.util + + +/** + * Temporary mapping for about content + */ +fun getAboutContent(key: String): String { + val aboutContent = buildMap { + put("104West!", "Cornell's kosher and multicultural dining room is STAR-K and STAR-D certified.") + put("Becker House Dining Room", "Dining room located in Carl Becker House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Cook House Dining Room", "Dining room located in Alice Cook House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Jansen's Dining Room at Bethe House", "Dining room located in Hans Bethe House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Keeton House Dining Room", "Dining room located in William Keeton House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Morrison Dining", "Choose your own culinary adventure at Cornell's newest dining room.") + put("North Star Dining Room", "Dining room located in Appel Commons on North Campus.") + put("Okenshields", "Dining room located in Willard Straight Hall on Central Campus.") + put("Risley Dining Room", "Risley is our gluten-free, tree nut free and peanut free dining room under the AllerCheckā„¢\uFE0F approved by MenuTrinfoĀ®\uFE0F program, in Risley Residential College on North Campus.") + put("Rose House Dining Room", "Dining room located in Flora Rose House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + } + + return aboutContent.getOrDefault(key, "") +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4ce9d0..c6488bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Help us improve Navi by letting us know what\'s wrong Covers humanities and social sciences with circulation and reference services. Reserve a room + View Menu on Eatery \ No newline at end of file From dbf58420b4e8ca9012d264c024d3c2f87bb1c581 Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Wed, 19 Nov 2025 00:19:05 -0500 Subject: [PATCH 3/6] Expanding operating hours toggle --- .../components/home/EateryDetailsContent.kt | 7 +- .../home/ExpandableOperatingHoursList.kt | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt index 0c512dd..64da8a8 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt @@ -121,9 +121,10 @@ fun EateryDetailsContent( HorizontalDivider(thickness = 1.dp, color = DividerGray) - Spacer(modifier = Modifier.height(24.dp)) - - OperatingHoursList(homeViewModel.rotateOperatingHours(eatery.formatOperatingHours())) + ExpandableOperatingHoursList( + homeViewModel.isOpenAnnotatedStringFromOperatingHours(eatery.formatOperatingHours()), + homeViewModel.rotateOperatingHours(eatery.formatOperatingHours()) + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt new file mode 100644 index 0000000..29e8a17 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt @@ -0,0 +1,79 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.clickable +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.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.R +import com.cornellappdev.transit.models.ecosystem.OperatingHours +import com.cornellappdev.transit.ui.theme.DividerGray +import com.cornellappdev.transit.ui.theme.Gray05 +import com.cornellappdev.transit.ui.theme.Style + +/** + * + */ +@Composable +fun ExpandableOperatingHoursList(annotatedString: AnnotatedString, operatingHours: OperatingHours) { + + var isExpanded by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { + isExpanded = !isExpanded + }) { + Icon( + painterResource(R.drawable.clock), + contentDescription = null, + modifier = Modifier + .size(20.dp), + tint = Gray05 + ) + Text( + text = annotatedString, + style = Style.detailBody, + modifier = Modifier.padding(start = 15.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + + if (isExpanded) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.baseline_expand_less), + contentDescription = "Expand less", + modifier = Modifier.height(79.dp) + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.baseline_expand_more), + contentDescription = "Expand more", + modifier = Modifier.height(79.dp) + ) + } + } + + if(isExpanded) { + OperatingHoursList(operatingHours) + } + HorizontalDivider(thickness = 1.dp, color = DividerGray) + } + +} \ No newline at end of file From f481b67b3a8c34ca95c86af8f1f23466abf58cc3 Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Wed, 19 Nov 2025 00:43:10 -0500 Subject: [PATCH 4/6] Revert unnecessary change --- .../java/com/cornellappdev/transit/ui/screens/RouteScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt index 07a1eff..f62d5d2 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.cornellappdev.transit.R import com.cornellappdev.transit.models.MapState @@ -176,7 +175,7 @@ fun RouteScreen( true, route ) - );navController.navigate("details") + ); navController.navigate("details") } } } From 1269b61e8652c6de05da0d9546296b51ecafa879 Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Wed, 19 Nov 2025 00:44:10 -0500 Subject: [PATCH 5/6] Remove unnecessary change 2 --- .../java/com/cornellappdev/transit/ui/screens/RouteScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt index f62d5d2..c6d8716 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt @@ -175,7 +175,7 @@ fun RouteScreen( true, route ) - ); navController.navigate("details") + );navController.navigate("details") } } } From b38f8189e8c1e2980fe736022235daf48f8ef30b Mon Sep 17 00:00:00 2001 From: Jonathan Chen <04jono@gmail.com> Date: Sun, 30 Nov 2025 23:04:07 -0500 Subject: [PATCH 6/6] PR comments --- .../models/ecosystem/DayOperatingHours.kt | 9 +++++ .../transit/models/ecosystem/Eatery.kt | 11 +++--- .../models/ecosystem/OperatingHours.kt | 3 -- .../home/DetailedPlaceHeaderSection.kt | 12 ++++++ .../home/EcosystemBottomSheetContent.kt | 13 ++++--- .../home/ExpandableOperatingHoursList.kt | 23 +++++++++-- .../ui/components/home/OperatingHoursList.kt | 38 +++++++------------ .../transit/ui/viewmodels/HomeViewModel.kt | 26 ++++++------- .../transit/util/PreviewConstants.kt | 24 ++++++++++++ .../cornellappdev/transit/util/TimeUtils.kt | 8 ++++ 10 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt delete mode 100644 app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt create mode 100644 app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt new file mode 100644 index 0000000..8d96a94 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt @@ -0,0 +1,9 @@ +package com.cornellappdev.transit.models.ecosystem + +/** + * Mapping of a day of the week to the hours for that day + */ +data class DayOperatingHours( + val dayOfWeek: String, + val hours: List +) diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt index f2a88e2..6367f8e 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt @@ -1,5 +1,6 @@ package com.cornellappdev.transit.models.ecosystem +import com.cornellappdev.transit.util.TimeUtils.toPascalCaseString import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import java.time.DayOfWeek @@ -45,20 +46,20 @@ data class Eatery( * and the corresponding times that an eatery is open. The list is sorted * by day with the custom dayOrder (Sunday first). */ - fun formatOperatingHours(): OperatingHours { + fun formatOperatingHours(): List { val dailyHours = operatingHours() // Convert map to list and sort by custom day order return dailyHours.entries .sortedBy { entry -> val dayName = - entry.key.name.take(1).uppercase() + entry.key.name.drop(1).lowercase() + entry.key.toPascalCaseString() dayOrder[dayName] ?: Int.MAX_VALUE } .map { entry -> val dayName = - entry.key.name.take(1).uppercase() + entry.key.name.drop(1).lowercase() - dayName to entry.value + entry.key.toPascalCaseString() + DayOperatingHours(dayName, entry.value) } } @@ -80,7 +81,7 @@ data class Eatery( } } - DayOfWeek.values().forEach { dayOfWeek -> + DayOfWeek.entries.forEach { dayOfWeek -> dailyHours.computeIfAbsent(dayOfWeek) { mutableListOf("Closed") } } diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt deleted file mode 100644 index 6a7bdb8..0000000 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/OperatingHours.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.cornellappdev.transit.models.ecosystem - -typealias OperatingHours = List>> diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt index ae2da23..2232832 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText @@ -79,4 +80,15 @@ fun DetailedPlaceHeaderSection( FavoritesStar(onFavoriteClick, isFavorite) } +} + +@Preview(showBackground = true) +@Composable +private fun DetailedPlaceHeaderSectionPreview() { + DetailedPlaceHeaderSection( + title = "Atrium Cafe", + subtitle = "Sage Hall", + onFavoriteClick = {}, + isFavorite = false + ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt index 69e3b11..d1eae12 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt @@ -22,8 +22,9 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.transit.R import com.cornellappdev.transit.models.Place +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours import com.cornellappdev.transit.models.ecosystem.DetailedEcosystemPlace -import com.cornellappdev.transit.models.ecosystem.OperatingHours +import com.cornellappdev.transit.models.ecosystem.Eatery import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.ui.theme.robotoFamily @@ -124,7 +125,7 @@ private fun BottomSheetFilteredContent( FilterState.EATERIES -> { eateryList( - staticPlaces = staticPlaces, + eateriesApiResponse = staticPlaces.eateries, onDetailsClick = onDetailsClick, favorites = favorites, onFavoriteStarClick = onFavoriteStarClick, @@ -222,13 +223,13 @@ private fun LazyListScope.printerList( * LazyList scoped enumeration of eateries for bottom sheet */ private fun LazyListScope.eateryList( - staticPlaces: StaticPlaces, + eateriesApiResponse: ApiResponse>, onDetailsClick: (DetailedEcosystemPlace) -> Unit, favorites: Set, onFavoriteStarClick: (Place) -> Unit, - operatingHoursToString: (OperatingHours) -> AnnotatedString + operatingHoursToString: (List) -> AnnotatedString ) { - when (staticPlaces.eateries) { + when (eateriesApiResponse) { is ApiResponse.Error -> { } @@ -239,7 +240,7 @@ private fun LazyListScope.eateryList( } is ApiResponse.Success -> { - items(staticPlaces.eateries.data) { + items(eateriesApiResponse.data) { RoundedImagePlaceCard( imageUrl = it.imageUrl, title = it.name, diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt index 29e8a17..d355140 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt @@ -22,18 +22,24 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.transit.R -import com.cornellappdev.transit.models.ecosystem.OperatingHours +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours import com.cornellappdev.transit.ui.theme.DividerGray import com.cornellappdev.transit.ui.theme.Gray05 import com.cornellappdev.transit.ui.theme.Style +import com.cornellappdev.transit.util.sampleHours /** * */ @Composable -fun ExpandableOperatingHoursList(annotatedString: AnnotatedString, operatingHours: OperatingHours) { +fun ExpandableOperatingHoursList( + annotatedString: AnnotatedString, + operatingHours: List +) { var isExpanded by remember { mutableStateOf(false) } @@ -70,10 +76,21 @@ fun ExpandableOperatingHoursList(annotatedString: AnnotatedString, operatingHour } } - if(isExpanded) { + if (isExpanded) { OperatingHoursList(operatingHours) } HorizontalDivider(thickness = 1.dp, color = DividerGray) } +} + +@Preview(showBackground = true) +@Composable +private fun ExpandableOperatingHoursListPreview() { + ExpandableOperatingHoursList( + annotatedString = buildAnnotatedString { + append("Open") + }, + operatingHours = sampleHours + ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt index 0af8b2f..4afd729 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt @@ -1,6 +1,5 @@ package com.cornellappdev.transit.ui.components.home -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,22 +7,21 @@ 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.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.cornellappdev.transit.models.ecosystem.OperatingHours +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style.heading3 import com.cornellappdev.transit.ui.theme.Style.heading3Semibold +import com.cornellappdev.transit.util.sampleHours /** * Composable that displays operating hours for an eatery @@ -33,7 +31,7 @@ import com.cornellappdev.transit.ui.theme.Style.heading3Semibold */ @Composable fun OperatingHoursList( - operatingHours: OperatingHours, + operatingHours: List, modifier: Modifier = Modifier ) { Column( @@ -67,10 +65,16 @@ private fun OperatingHoursRow( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top ) { - Row (horizontalArrangement = Arrangement.Start, - modifier = Modifier.weight(0.6f)) { + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.weight(0.6f) + ) { if (isHighlighted) { - VerticalDivider(color = PrimaryText, thickness = 2.dp, modifier = Modifier.height(16.dp)) + VerticalDivider( + color = PrimaryText, + thickness = 2.dp, + modifier = Modifier.height(16.dp) + ) Spacer(Modifier.width(12.dp)) } else { Spacer(Modifier.width(14.dp)) @@ -100,25 +104,9 @@ private fun OperatingHoursRow( } } -@Preview +@Preview(showBackground = true) @Composable private fun OperatingHoursPreview() { - val sampleHours = listOf( - "Tuesday" to listOf("10:00 AM - 10:00 PM"), - "Wednesday" to listOf("10:00 AM - 5:00 PM"), - "Thursday" to listOf("Closed"), - "Friday" to listOf("10:00 AM - 5:00 PM"), - "Saturday" to listOf( - "8:00 AM - 9:30 AM", - "10:30 AM - 2:00 PM", - "5:00 PM - 8:00 PM" - ), - "Sunday" to listOf( - "10:00 AM - 2:00 PM", - "5:00 PM - 8:30 PM" - ), - "Monday" to listOf("10:00 AM - 10:00 PM") - ) OperatingHoursList(operatingHours = sampleHours) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt index a4c8b0a..53f373c 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt @@ -14,14 +14,15 @@ import com.cornellappdev.transit.models.RouteRepository import com.cornellappdev.transit.models.SelectedRouteRepository import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.models.UserPreferenceRepository +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours import com.cornellappdev.transit.models.ecosystem.EateryRepository import com.cornellappdev.transit.models.ecosystem.GymRepository -import com.cornellappdev.transit.models.ecosystem.OperatingHours import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.ui.theme.LateRed import com.cornellappdev.transit.ui.theme.LiveGreen import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style.cardSubtitle +import com.cornellappdev.transit.util.TimeUtils.toPascalCaseString import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -295,14 +296,13 @@ class HomeViewModel @Inject constructor( * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open */ fun rotateOperatingHours( - operatingHours: OperatingHours, + operatingHours: List, currentDate: LocalDate = LocalDate.now() - ): OperatingHours { - val today = currentDate.dayOfWeek.name.lowercase() - .replaceFirstChar { it.uppercase() } + ): List { + val today = currentDate.dayOfWeek.toPascalCaseString() val todayIndex = operatingHours.indexOfFirst { - it.first.equals(today, ignoreCase = true) + it.dayOfWeek.equals(today, ignoreCase = true) } // Defensive programming only if [operatingHours] is missing a day @@ -314,9 +314,9 @@ class HomeViewModel @Inject constructor( /** * Find the next time a place is open if it is closed for the day */ - private fun findOpenNextDay(operatingHours: OperatingHours): OpenStatus { + private fun findOpenNextDay(operatingHours: List): OpenStatus { // Check day after - val dayAfter = operatingHours[1].second + val dayAfter = operatingHours[1].hours if (!dayAfter.any { it.equals("Closed", ignoreCase = true) }) { val firstOpenTime = parseTimeRange(dayAfter[0])?.first if (firstOpenTime != null) { @@ -328,9 +328,9 @@ class HomeViewModel @Inject constructor( } // Find next open day for (i in 2 until operatingHours.size) { - val currDay = operatingHours[i].second + val currDay = operatingHours[i].hours if (!currDay.any { it.equals("Closed", ignoreCase = true) }) { - val dayName = operatingHours[i].first + val dayName = operatingHours[i].dayOfWeek return OpenStatus( false, "until $dayName" @@ -347,12 +347,12 @@ class HomeViewModel @Inject constructor( * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open */ private fun getOpenStatus( - operatingHours: OperatingHours, + operatingHours: List, currentDateTime: LocalDateTime = LocalDateTime.now() ): OpenStatus { val currentTime = currentDateTime.toLocalTime() - val todaySchedule = operatingHours[0].second // First day should be today after rotation + val todaySchedule = operatingHours[0].hours // First day should be today after rotation // Check if closed today if (todaySchedule.any { it.equals("Closed", ignoreCase = true) }) { @@ -436,7 +436,7 @@ class HomeViewModel @Inject constructor( /** * Rotate operating hours to current day, then determine if place is open, then format string */ - fun isOpenAnnotatedStringFromOperatingHours(operatingHours: OperatingHours): AnnotatedString { + fun isOpenAnnotatedStringFromOperatingHours(operatingHours: List): AnnotatedString { return getOpenStatusAnnotatedString( getOpenStatus( rotateOperatingHours(operatingHours) diff --git a/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt new file mode 100644 index 0000000..3f7c01f --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt @@ -0,0 +1,24 @@ +package com.cornellappdev.transit.util + +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours + +val sampleHours = listOf( + DayOperatingHours("Tuesday", listOf("10:00 AM - 10:00 PM")), + DayOperatingHours("Wednesday", listOf("10:00 AM - 5:00 PM")), + DayOperatingHours("Thursday", listOf("Closed")), + DayOperatingHours("Friday", listOf("10:00 AM - 5:00 PM")), + DayOperatingHours( + "Saturday", listOf( + "8:00 AM - 9:30 AM", + "10:30 AM - 2:00 PM", + "5:00 PM - 8:00 PM" + ) + ), + DayOperatingHours( + "Sunday", listOf( + "10:00 AM - 2:00 PM", + "5:00 PM - 8:30 PM" + ) + ), + DayOperatingHours("Monday", listOf("10:00 AM - 10:00 PM")), +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt index 8ea5e3b..34642b0 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt @@ -1,6 +1,7 @@ package com.cornellappdev.transit.util import android.icu.text.SimpleDateFormat +import java.time.DayOfWeek import java.time.Duration import java.time.Instant import java.time.ZoneId @@ -93,4 +94,11 @@ object TimeUtils { return Instant.parse(isoString).plusSeconds(seconds.toLong()).toString() } + /** + * Day of week with first letter capitalized and all else lowercase + */ + fun DayOfWeek.toPascalCaseString(): String = this.name.lowercase() + .replaceFirstChar { it.uppercase() } + + } \ No newline at end of file