From f205f09da13b98b2ef47cc512fb537b6ee23cd61 Mon Sep 17 00:00:00 2001 From: Andrew Cheung Date: Sun, 16 Nov 2025 03:02:59 -0500 Subject: [PATCH 1/5] home ui foundation: general components, icons, service model, constants, icon assets, composeBom dependency update, bottom navigation update, minor design system updates --- .../hustle/data/model/services/Service.kt | 17 + .../ui/components/general/BackButton.kt | 27 ++ .../general/ClickableSectionHeader.kt | 55 ++++ .../ui/components/general/HustleButton.kt | 88 +++++ .../ui/components/general/HustleSearchBar.kt | 133 ++++++++ .../components/general/UserProfilePicture.kt | 38 +++ .../general/service/FavoriteButton.kt | 92 ++++++ .../components/general/service/ServiceCard.kt | 310 ++++++++++++++++++ .../ServiceHorizontalCarouselSection.kt | 78 +++++ .../general/service/ServiceRatingLabel.kt | 48 +++ .../hustle/ui/navigation/BottomNavigation.kt | 34 +- .../hustle/ui/navigation/HustleNavigation.kt | 2 + .../navigation/navgraphs/LearnNavigation.kt | 14 + .../cornellappdev/hustle/ui/theme/Color.kt | 12 +- .../cornellappdev/hustle/ui/theme/Theme.kt | 8 +- .../hustle/util/constants/HustleConstants.kt | 16 + .../hustle/util/constants/TestingConstants.kt | 101 ++++++ app/src/main/res/drawable/ic_beauty.xml | 9 + app/src/main/res/drawable/ic_chevron.xml | 9 + .../main/res/drawable/ic_favorite_filled.xml | 11 + .../main/res/drawable/ic_favorite_outline.xml | 12 + app/src/main/res/drawable/ic_history.xml | 9 + app/src/main/res/drawable/ic_learn.xml | 13 + app/src/main/res/drawable/ic_lessons.xml | 9 + app/src/main/res/drawable/ic_photo.xml | 9 + app/src/main/res/drawable/ic_professional.xml | 9 + app/src/main/res/drawable/ic_rating_star.xml | 9 + .../main/res/drawable/ic_search_leading.xml | 20 ++ gradle/libs.versions.toml | 4 +- 29 files changed, 1178 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/hustle/data/model/services/Service.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/BackButton.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/ClickableSectionHeader.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleSearchBar.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/UserProfilePicture.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/LearnNavigation.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt create mode 100644 app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt create mode 100644 app/src/main/res/drawable/ic_beauty.xml create mode 100644 app/src/main/res/drawable/ic_chevron.xml create mode 100644 app/src/main/res/drawable/ic_favorite_filled.xml create mode 100644 app/src/main/res/drawable/ic_favorite_outline.xml create mode 100644 app/src/main/res/drawable/ic_history.xml create mode 100644 app/src/main/res/drawable/ic_learn.xml create mode 100644 app/src/main/res/drawable/ic_lessons.xml create mode 100644 app/src/main/res/drawable/ic_photo.xml create mode 100644 app/src/main/res/drawable/ic_professional.xml create mode 100644 app/src/main/res/drawable/ic_rating_star.xml create mode 100644 app/src/main/res/drawable/ic_search_leading.xml diff --git a/app/src/main/java/com/cornellappdev/hustle/data/model/services/Service.kt b/app/src/main/java/com/cornellappdev/hustle/data/model/services/Service.kt new file mode 100644 index 0000000..61566a4 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/data/model/services/Service.kt @@ -0,0 +1,17 @@ +package com.cornellappdev.hustle.data.model.services + +import com.cornellappdev.hustle.data.model.user.User + +//TODO: Update model fields once API is finalized +data class Service( + val id: Int, + val name: String, + val category: String, + val minimumPrice: Double, + val priceUnit: String = "", + val rating: Double, + val displayImageUrl: String, + val isFavorited: Boolean, + val user: User +) + diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/BackButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/BackButton.kt new file mode 100644 index 0000000..d4e15fb --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/BackButton.kt @@ -0,0 +1,27 @@ +package com.cornellappdev.hustle.ui.components.general + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.cornellappdev.hustle.R + +@Composable +fun BackButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + IconButton(onClick = onClick, modifier = modifier) { + Icon( + painter = painterResource(R.drawable.ic_chevron), + contentDescription = "Back Button", + tint = Color.Unspecified + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BackButtonPreview() { + BackButton(onClick = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/ClickableSectionHeader.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/ClickableSectionHeader.kt new file mode 100644 index 0000000..f536a1d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/ClickableSectionHeader.kt @@ -0,0 +1,55 @@ +package com.cornellappdev.hustle.ui.components.general + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Icon +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.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.ui.theme.HustleTheme + +@Composable +fun ClickableSectionHeader( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.headlineSmall +) { + Row( + modifier = modifier.clickable(onClick = onClick), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = textStyle + ) + Icon( + painter = painterResource(R.drawable.ic_chevron), + contentDescription = "Chevron Icon", + tint = Color.Unspecified, + modifier = Modifier.rotate(180f) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ClickableSectionHeaderPreview() { + HustleTheme { + ClickableSectionHeader( + title = "Popular right now", + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt new file mode 100644 index 0000000..67d8067 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt @@ -0,0 +1,88 @@ +package com.cornellappdev.hustle.ui.components.general + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.ui.theme.HustleColors +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme + +@Composable +fun HustleButton( + onClick: () -> Unit, + text: String?, + textStyle: TextStyle = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = RoundedCornerShape(20.dp), + border: BorderStroke? = null, + buttonColors: ButtonColors = ButtonDefaults.buttonColors( + containerColor = HustleColors.hustleGreen, + contentColor = HustleColors.white + ), + contentPadding: PaddingValues = PaddingValues( + horizontal = HustleSpacing.small, + vertical = HustleSpacing.extraSmall + ), + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = shape, + border = border, + colors = buttonColors, + contentPadding = contentPadding + ) { + leadingIcon?.let { + leadingIcon() + if (text != null) Spacer(modifier = Modifier.width(4.dp)) + } + text?.let { + Text( + text = it, + style = textStyle + ) + } + trailingIcon?.let { + if (text != null) Spacer(modifier = Modifier.width(4.dp)) + trailingIcon() + } + } +} + +@Preview(showBackground = true) +@Composable +fun HustleButtonPreview() { + HustleTheme { + HustleButton( + onClick = {}, + text = "Lessons", + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_lessons), + contentDescription = "Lessons Icon" + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleSearchBar.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleSearchBar.kt new file mode 100644 index 0000000..072cdbc --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleSearchBar.kt @@ -0,0 +1,133 @@ +package com.cornellappdev.hustle.ui.components.general + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +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.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.ui.theme.HustleColors +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme + +@Composable +fun HustleSearchBar( + queryState: TextFieldState, + isSearchActive: Boolean, + onFocus: () -> Unit, + onSearch: () -> Unit, + modifier: Modifier = Modifier +) { + val focusManager = LocalFocusManager.current + TextField( + state = queryState, + lineLimits = TextFieldLineLimits.SingleLine, + textStyle = MaterialTheme.typography.labelLarge.copy( + color = HustleColors.secondaryGray, + lineHeight = 18.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ), + ), + modifier = modifier + .fillMaxWidth() + .height(32.dp) + .onFocusChanged { if (it.isFocused) onFocus() }, + shape = RoundedCornerShape(20.dp), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedContainerColor = HustleColors.shadedGray.copy(alpha = 0.47f), + unfocusedContainerColor = HustleColors.shadedGray.copy(alpha = 0.47f), + focusedTextColor = HustleColors.secondaryGray, + unfocusedTextColor = HustleColors.secondaryGray, + cursorColor = HustleColors.secondaryGray + ), + contentPadding = PaddingValues( + horizontal = HustleSpacing.medium, + vertical = HustleSpacing.extraSmall + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + onKeyboardAction = { + onSearch() + focusManager.clearFocus() + }, + placeholder = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(HustleSpacing.extraSmall) + ) { + AnimatedVisibility( + visible = !isSearchActive, + enter = scaleIn() + fadeIn(), + exit = fadeOut(animationSpec = tween(100)) + shrinkHorizontally( + shrinkTowards = Alignment.Start, + animationSpec = tween(100) + ) + ) { + Icon( + painter = painterResource(R.drawable.ic_search_leading), + contentDescription = "Search Leading Icon", + tint = Color.Unspecified + ) + } + Text( + text = "Search services", + style = MaterialTheme.typography.labelLarge, + color = HustleColors.wash + ) + } + } + ) +} + + +@Preview(showBackground = true) +@Composable +private fun HustleSearchBarPreview() { + val queryState = rememberTextFieldState() + var isSearchActive by remember { mutableStateOf(false) } + HustleTheme { + HustleSearchBar( + queryState = queryState, + isSearchActive = isSearchActive, + onFocus = { isSearchActive = true }, + onSearch = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/UserProfilePicture.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/UserProfilePicture.kt new file mode 100644 index 0000000..d6554a0 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/UserProfilePicture.kt @@ -0,0 +1,38 @@ +package com.cornellappdev.hustle.ui.components.general + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.SubcomposeAsyncImage +import com.cornellappdev.hustle.ui.theme.HustleColors + +@Composable +fun UserProfilePicture( + imageUrl: String, + modifier: Modifier = Modifier +) { + // TODO: Add loading and error states + SubcomposeAsyncImage( + model = imageUrl, + contentDescription = "User Profile Picture", + contentScale = ContentScale.Crop, + modifier = modifier.clip(shape = CircleShape) + ) +} + +@Preview(showBackground = true) +@Composable +private fun UserProfilePicturePreview() { + UserProfilePicture( + imageUrl = "", + modifier = Modifier + .size(22.dp) + .border(1.dp, HustleColors.secondaryGray, CircleShape) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt new file mode 100644 index 0000000..a0eac1e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt @@ -0,0 +1,92 @@ +package com.cornellappdev.hustle.ui.components.general.service + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.R +import kotlinx.coroutines.launch + +@Composable +fun FavoriteButton( + isFavorite: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val fillAlpha by animateFloatAsState( + targetValue = if (isFavorite) 1f else 0f, + animationSpec = tween(200), + label = "Fill Alpha" + ) + + val scale = remember { Animatable(1f) } + val coroutineScope = rememberCoroutineScope() + + IconButton( + onClick = { + if (!isFavorite) { + coroutineScope.launch { + // Animate the button to scale up and then back to normal size + scale.animateTo(1.2f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + scale.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + } + } + onClick() + }, + modifier = modifier + ) { + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = scale.value + scaleY = scale.value + }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.ic_favorite_outline), + contentDescription = "Favorite Outline", + tint = Color.Unspecified + ) + + Icon( + painter = painterResource(R.drawable.ic_favorite_filled), + contentDescription = "Favorite Filled", + tint = Color.Unspecified, + modifier = Modifier.graphicsLayer { alpha = fillAlpha } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun FavoriteButtonPreview() { + var isFavorite by remember { mutableStateOf(false) } + + FavoriteButton( + isFavorite = isFavorite, + onClick = { isFavorite = !isFavorite }, + modifier = Modifier.size(32.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt new file mode 100644 index 0000000..b8ba5ff --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt @@ -0,0 +1,310 @@ +package com.cornellappdev.hustle.ui.components.general.service + +import androidx.compose.foundation.BorderStroke +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.fillMaxSize +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.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.SubcomposeAsyncImage +import com.cornellappdev.hustle.ui.components.general.UserProfilePicture +import com.cornellappdev.hustle.ui.theme.HustleColors +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme +import com.cornellappdev.hustle.util.constants.TEST_SERVICES +import kotlin.math.ceil + +enum class ServiceCardType { + DEFAULT, + RESULT +} + +@Composable +fun ServiceCard( + serviceImageUrl: String, + userName: String, + userProfileImageUrl: String, + serviceTitle: String, + serviceMinimumPriceCeiling: Int, + priceUnit: String, + serviceRating: Double, + onClick: () -> Unit, + isFavorite: Boolean? = null, + onFavoriteClick: (() -> Unit)? = null, + cardType: ServiceCardType = ServiceCardType.DEFAULT, + modifier: Modifier = Modifier +) { + val cardHeight = when (cardType) { + ServiceCardType.DEFAULT -> 279.dp + ServiceCardType.RESULT -> 437.dp + } + val imageHeight = when (cardType) { + ServiceCardType.DEFAULT -> 175.dp + ServiceCardType.RESULT -> 358.dp + } + Card( + onClick = onClick, + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors( + containerColor = HustleColors.white, + contentColor = HustleColors.primaryBlack + ), + border = BorderStroke(width = 1.dp, color = HustleColors.iconInactive), + modifier = modifier.height(cardHeight) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(imageHeight) + ) { + // TODO: Add loading and error states + SubcomposeAsyncImage( + model = serviceImageUrl, + contentDescription = "Service Image", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + if (isFavorite != null && onFavoriteClick != null) { + FavoriteButton( + isFavorite = isFavorite, + onClick = onFavoriteClick, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(HustleSpacing.small) + .size(32.dp) + ) + } + } + + when (cardType) { + ServiceCardType.DEFAULT -> { + DefaultServiceCardContent( + userName = userName, + userProfileImageUrl = userProfileImageUrl, + serviceTitle = serviceTitle, + serviceMinimumPriceCeiling = serviceMinimumPriceCeiling, + priceUnit = priceUnit, + serviceRating = serviceRating + ) + } + + ServiceCardType.RESULT -> { + ResultServiceCardContent( + userName = userName, + userProfileImageUrl = userProfileImageUrl, + serviceTitle = serviceTitle, + serviceMinimumPriceCeiling = serviceMinimumPriceCeiling, + priceUnit = priceUnit, + serviceRating = serviceRating + ) + } + } + } +} + +@Composable +private fun DefaultServiceCardContent( + userName: String, + userProfileImageUrl: String, + serviceTitle: String, + serviceMinimumPriceCeiling: Int, + priceUnit: String, + serviceRating: Double, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = modifier + .fillMaxWidth() + .padding( + top = HustleSpacing.extraSmall, + bottom = HustleSpacing.medium, + start = 14.dp, + end = 14.dp + ) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(HustleSpacing.extraSmall), + verticalAlignment = Alignment.CenterVertically + ) { + UserProfilePicture( + imageUrl = userProfileImageUrl, + modifier = Modifier.size(22.dp) + ) + Text( + text = userName, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold + ), + lineHeight = 14.sp + ) + } + + Text( + text = serviceTitle, + style = MaterialTheme.typography.labelLarge, + maxLines = 2, + lineHeight = 14.sp, + overflow = TextOverflow.Ellipsis + ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "From \$$serviceMinimumPriceCeiling$priceUnit", + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 106.dp) + ) + ServiceRatingLabel(rating = serviceRating) + } + } +} + +@Composable +private fun ResultServiceCardContent( + userName: String, + userProfileImageUrl: String, + serviceTitle: String, + serviceMinimumPriceCeiling: Int, + priceUnit: String, + serviceRating: Double, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 20.dp, horizontal = HustleSpacing.medium) + ) { + UserProfilePicture( + imageUrl = userProfileImageUrl, + modifier = Modifier.size(39.dp) + ) + + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier + .widthIn(max = 175.dp) + .padding(start = HustleSpacing.medium) + ) { + Text( + text = userName, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold + ), + lineHeight = 14.sp + ) + Text( + text = serviceTitle, + style = MaterialTheme.typography.labelLarge, + color = HustleColors.secondaryGray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + lineHeight = 14.sp + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + ServiceRatingLabel( + rating = serviceRating, textStyle = MaterialTheme.typography.labelLarge.copy( + color = HustleColors.secondaryGray + ) + ) + Text( + text = "From \$$serviceMinimumPriceCeiling$priceUnit", + style = MaterialTheme.typography.labelLarge, + color = HustleColors.secondaryGray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + } +} + +data class ServiceCardPreviewParameters( + val isFavorite: Boolean? = null, + val onFavoriteClick: (() -> Unit)? = null, + val cardType: ServiceCardType = ServiceCardType.DEFAULT, + val modifier: Modifier = Modifier +) + +class ServiceCardPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ServiceCardPreviewParameters( + isFavorite = true, + onFavoriteClick = {}, + modifier = Modifier.width(178.dp) + ), + ServiceCardPreviewParameters( + isFavorite = false, + onFavoriteClick = {}, + modifier = Modifier.width(178.dp) + ), + ServiceCardPreviewParameters( + cardType = ServiceCardType.RESULT, + modifier = Modifier.width(365.dp) + ) + ) +} + +@Preview(showBackground = true) +@Composable +private fun ServiceCardPreview( + @PreviewParameter(ServiceCardPreviewParameterProvider::class) parameters: ServiceCardPreviewParameters +) { + val testService = TEST_SERVICES[0] + HustleTheme { + ServiceCard( + serviceImageUrl = testService.displayImageUrl, + userName = testService.user.displayName ?: "", + userProfileImageUrl = testService.user.photoUrl ?: "", + serviceTitle = testService.name, + serviceMinimumPriceCeiling = ceil(testService.minimumPrice).toInt(), + priceUnit = testService.priceUnit, + serviceRating = testService.rating, + onClick = {}, + isFavorite = parameters.isFavorite, + onFavoriteClick = parameters.onFavoriteClick, + cardType = parameters.cardType, + modifier = parameters.modifier + ) + } +} + + diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt new file mode 100644 index 0000000..7d339aa --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt @@ -0,0 +1,78 @@ +package com.cornellappdev.hustle.ui.components.general.service + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.carousel.HorizontalUncontainedCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.data.model.services.Service +import com.cornellappdev.hustle.ui.components.general.ClickableSectionHeader +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme +import com.cornellappdev.hustle.util.constants.TEST_SERVICES +import kotlin.math.ceil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ServiceHorizontalCarouselSection( + serviceListings: List, + onServiceClick: (Int) -> Unit, + onFavoriteClick: (Int) -> Unit, + modifier: Modifier = Modifier, + header: @Composable () -> Unit = {}, +) { + val carouselState = rememberCarouselState() { serviceListings.size } + Column( + verticalArrangement = Arrangement.spacedBy(HustleSpacing.medium), + modifier = modifier + ) { + header() + HorizontalUncontainedCarousel( + state = carouselState, + itemWidth = 182.dp, + itemSpacing = HustleSpacing.small, + contentPadding = PaddingValues(horizontal = HustleSpacing.large), + ) { i -> + val service = serviceListings[i] + ServiceCard( + serviceImageUrl = service.displayImageUrl, + userName = service.user.displayName ?: "", + userProfileImageUrl = service.user.photoUrl ?: "", + serviceTitle = service.name, + serviceMinimumPriceCeiling = ceil(service.minimumPrice).toInt(), + priceUnit = service.priceUnit, + serviceRating = service.rating, + onClick = { onServiceClick(service.id) }, + isFavorite = service.isFavorited, + onFavoriteClick = { onFavoriteClick(service.id) }, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } + + } +} + +@Preview(showBackground = true) +@Composable +private fun ServiceHorizontalCarouselPreview() { + HustleTheme { + ServiceHorizontalCarouselSection( + serviceListings = TEST_SERVICES, + onServiceClick = {}, + onFavoriteClick = {}, + header = { + ClickableSectionHeader( + title = "Recently viewed", + onClick = {}, + modifier = Modifier.padding(start = 24.dp) + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt new file mode 100644 index 0000000..a0c6946 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt @@ -0,0 +1,48 @@ +package com.cornellappdev.hustle.ui.components.general.service + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Icon +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.ui.theme.HustleTheme + +@Composable +fun ServiceRatingLabel( + rating: Double, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.labelLarge +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rating_star), + contentDescription = "Star Icon", + tint = Color.Unspecified + ) + Text( + text = String.format("%.1f", rating), + style = textStyle + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ServiceRatingLabelPreview() { + HustleTheme { + ServiceRatingLabel(rating = 4.8) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt index 64b88d8..fc390fa 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/BottomNavigation.kt @@ -2,8 +2,10 @@ package com.cornellappdev.hustle.ui.navigation import androidx.annotation.DrawableRes import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -15,6 +17,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.ui.theme.HustleColors data class BottomNavigationItem( val route: T, @@ -30,6 +33,12 @@ val bottomNavigationItems = listOf( icon = R.drawable.ic_home, selectedIcon = R.drawable.ic_home, ), + BottomNavigationItem( + route = LearnTab, + title = "Learn", + icon = R.drawable.ic_learn, + selectedIcon = R.drawable.ic_learn, + ), BottomNavigationItem( route = MessagesTab, title = "Messages", @@ -56,17 +65,19 @@ fun BottomNavigationBar(navController: NavHostController) { } if (shouldShow) { - NavigationBar { + NavigationBar( + containerColor = MaterialTheme.colorScheme.background, + ) { bottomNavigationItems.forEach { item -> val selected = currentDestination?.hierarchy?.any { it.hasRoute(item.route::class) } == true - NavigationBarItem(icon = { + NavigationBarItem( + icon = { Icon( painter = painterResource(id = if (selected) item.selectedIcon else item.icon), - contentDescription = item.title, - tint = Color.Unspecified + contentDescription = item.title ) }, selected = selected, onClick = { navController.navigate(item.route) { @@ -77,8 +88,19 @@ fun BottomNavigationBar(navController: NavHostController) { restoreState = true } }, label = { - Text(item.title) - }) + Text( + item.title, + style = MaterialTheme.typography.labelMedium + ) + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = HustleColors.hustleGreen, + unselectedIconColor = Color.Unspecified, + selectedTextColor = HustleColors.hustleGreen, + unselectedTextColor = HustleColors.secondaryGray, + indicatorColor = Color.Transparent + ) + ) } } } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt index 58649d6..7ecd607 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.cornellappdev.hustle.ui.navigation.navgraphs.homeNavGraph +import com.cornellappdev.hustle.ui.navigation.navgraphs.learnNavGraph import com.cornellappdev.hustle.ui.navigation.navgraphs.messagesNavGraph import com.cornellappdev.hustle.ui.navigation.navgraphs.onboardingNavGraph import com.cornellappdev.hustle.ui.navigation.navgraphs.profileNavGraph @@ -45,6 +46,7 @@ fun HustleNavigation( ) { onboardingNavGraph(navController = navController) homeNavGraph(navController = navController) + learnNavGraph(navController = navController) messagesNavGraph(navController = navController) profileNavGraph(navController = navController) } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/LearnNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/LearnNavigation.kt new file mode 100644 index 0000000..4dc10f9 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/LearnNavigation.kt @@ -0,0 +1,14 @@ +package com.cornellappdev.hustle.ui.navigation.navgraphs + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.cornellappdev.hustle.ui.navigation.LearnDestination +import com.cornellappdev.hustle.ui.navigation.LearnTab + +fun NavGraphBuilder.learnNavGraph(navController: NavHostController) { + navigation(startDestination = LearnDestination.Workshops) { + composable {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/theme/Color.kt b/app/src/main/java/com/cornellappdev/hustle/ui/theme/Color.kt index 1ae6fd9..ba2c27e 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/theme/Color.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/theme/Color.kt @@ -5,12 +5,12 @@ import androidx.compose.ui.graphics.Color object HustleColors { val hustleGreen = Color(0xFF004346) val accentGreen = Color(0xFFD5EFB4) + val ratingGreen = Color(0xFFAFCB8B) + val shadingGreen = accentGreen.copy(alpha = 0.45f) val primaryBlack = Color(0xFF000000) - val tertiaryGray = Color(0xFF2D2D2D) - val secondaryGray = Color(0xFF7D8288) - val iconInactive = Color(0xFFBEBEBE) - val stroke = Color(0xFFD6D6D6) - val wash = Color(0xFFF5F5F5) - val tint = Color(0xFFB2B3B6) + val secondaryGray = Color(0xFF636161) + val iconInactive = Color(0xFFD9D9D9) + val shadedGray = Color(0xFFD6D6D6) + val wash = Color(0xFF958F8F) val white = Color(0xFFFFFFFF) } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/theme/Theme.kt b/app/src/main/java/com/cornellappdev/hustle/ui/theme/Theme.kt index cf8e5e5..6dc02ae 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/theme/Theme.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/theme/Theme.kt @@ -21,10 +21,10 @@ private val DarkColorScheme = darkColorScheme( onSurfaceVariant = HustleColors.secondaryGray, surfaceVariant = HustleColors.wash, surfaceContainer = HustleColors.wash, - surfaceContainerHigh = HustleColors.stroke, + surfaceContainerHigh = HustleColors.shadedGray, background = HustleColors.white, onBackground = HustleColors.primaryBlack, - outline = HustleColors.stroke, + outline = HustleColors.shadedGray, outlineVariant = HustleColors.iconInactive ) @@ -38,10 +38,10 @@ private val LightColorScheme = lightColorScheme( onSurfaceVariant = HustleColors.secondaryGray, surfaceVariant = HustleColors.wash, surfaceContainer = HustleColors.wash, - surfaceContainerHigh = HustleColors.stroke, + surfaceContainerHigh = HustleColors.shadedGray, background = HustleColors.white, onBackground = HustleColors.primaryBlack, - outline = HustleColors.stroke, + outline = HustleColors.shadedGray, outlineVariant = HustleColors.iconInactive /* Other default colors to override diff --git a/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt b/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt new file mode 100644 index 0000000..831de33 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt @@ -0,0 +1,16 @@ +package com.cornellappdev.hustle.util.constants + +import androidx.annotation.DrawableRes +import com.cornellappdev.hustle.R + +data class ServiceCategory( + val name: String, @DrawableRes + val iconResId: Int +) + +val SERVICE_CATEGORIES = listOf( + ServiceCategory("Lessons", R.drawable.ic_lessons), + ServiceCategory("Photo", R.drawable.ic_photo), + ServiceCategory("Beauty", R.drawable.ic_beauty), + ServiceCategory("Professional", R.drawable.ic_professional) +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt b/app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt new file mode 100644 index 0000000..5ae0e66 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt @@ -0,0 +1,101 @@ +package com.cornellappdev.hustle.util.constants + +import com.cornellappdev.hustle.data.model.services.Service +import com.cornellappdev.hustle.data.model.user.User + +val TEST_RECENT_SEARCHES = listOf( + "nails", + "photos", + "haircuts", + "tutors", + "resumes", + "art", + "programming", + "moving", + "cleaning", + "cooking" +) + +val TEST_SERVICES = listOf( + Service( + id = 1, + name = "Dreamy fall grad photo session", + category = "Photo", + minimumPrice = 67.0, + rating = 4.1, + isFavorited = false, + user = User( + firebaseUid = "", + email = "", + displayName = "Jane Doe", + photoUrl = "https://lh3.googleusercontent.com/a/ACg8ocKJrWoJxoOC0CoGv76ocYAULrRz9dAlfxMOiTb78E5dXH1VVo_j=s576-c-no" + ), + displayImageUrl = "https://news.cornell.edu/sites/default/files/styles/full_size/public/06_2023_1114_sh_005-n_1.jpg?itok=E3ecxgYl", + priceUnit = "/hour", + ), + Service( + id = 2, + name = "Dreamy fall grad photo session", + category = "Photo", + minimumPrice = 67.0, + rating = 4.1, + isFavorited = false, + user = User( + firebaseUid = "", + email = "", + displayName = "Jane Doe", + photoUrl = "https://lh3.googleusercontent.com/a/ACg8ocKJrWoJxoOC0CoGv76ocYAULrRz9dAlfxMOiTb78E5dXH1VVo_j=s576-c-no" + ), + displayImageUrl = "https://news.cornell.edu/sites/default/files/styles/full_size/public/06_2023_1114_sh_005-n_1.jpg?itok=E3ecxgYl", + priceUnit = "/hour", + ), + Service( + id = 3, + name = "Dreamy fall grad photo session", + category = "Photo", + minimumPrice = 67.0, + rating = 4.1, + isFavorited = false, + user = User( + firebaseUid = "", + email = "", + displayName = "Jane Doe", + photoUrl = "https://lh3.googleusercontent.com/a/ACg8ocKJrWoJxoOC0CoGv76ocYAULrRz9dAlfxMOiTb78E5dXH1VVo_j=s576-c-no" + ), + displayImageUrl = "https://news.cornell.edu/sites/default/files/styles/full_size/public/06_2023_1114_sh_005-n_1.jpg?itok=E3ecxgYl", + priceUnit = "/hour", + ), + Service( + id = 4, + name = "Dreamy fall grad photo session", + category = "Photo", + minimumPrice = 67.0, + rating = 4.1, + isFavorited = false, + user = User( + firebaseUid = "", + email = "", + displayName = "Jane Doe", + photoUrl = "https://lh3.googleusercontent.com/a/ACg8ocKJrWoJxoOC0CoGv76ocYAULrRz9dAlfxMOiTb78E5dXH1VVo_j=s576-c-no" + ), + displayImageUrl = "https://news.cornell.edu/sites/default/files/styles/full_size/public/06_2023_1114_sh_005-n_1.jpg?itok=E3ecxgYl", + priceUnit = "/hour", + ), + Service( + id = 5, + name = "Dreamy fall grad photo session", + category = "Photo", + minimumPrice = 67.0, + rating = 4.1, + isFavorited = false, + user = User( + firebaseUid = "", + email = "", + displayName = "Jane Doe", + photoUrl = "https://lh3.googleusercontent.com/a/ACg8ocKJrWoJxoOC0CoGv76ocYAULrRz9dAlfxMOiTb78E5dXH1VVo_j=s576-c-no" + ), + displayImageUrl = "https://news.cornell.edu/sites/default/files/styles/full_size/public/06_2023_1114_sh_005-n_1.jpg?itok=E3ecxgYl", + priceUnit = "/hour", + ), + +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_beauty.xml b/app/src/main/res/drawable/ic_beauty.xml new file mode 100644 index 0000000..99fde9b --- /dev/null +++ b/app/src/main/res/drawable/ic_beauty.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron.xml b/app/src/main/res/drawable/ic_chevron.xml new file mode 100644 index 0000000..53d49da --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_filled.xml b/app/src/main/res/drawable/ic_favorite_filled.xml new file mode 100644 index 0000000..7e5a8f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_filled.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_outline.xml b/app/src/main/res/drawable/ic_favorite_outline.xml new file mode 100644 index 0000000..4aaa219 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_outline.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..ac90226 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_learn.xml b/app/src/main/res/drawable/ic_learn.xml new file mode 100644 index 0000000..5193af5 --- /dev/null +++ b/app/src/main/res/drawable/ic_learn.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_lessons.xml b/app/src/main/res/drawable/ic_lessons.xml new file mode 100644 index 0000000..261eb5f --- /dev/null +++ b/app/src/main/res/drawable/ic_lessons.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_photo.xml b/app/src/main/res/drawable/ic_photo.xml new file mode 100644 index 0000000..548b943 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_professional.xml b/app/src/main/res/drawable/ic_professional.xml new file mode 100644 index 0000000..9824313 --- /dev/null +++ b/app/src/main/res/drawable/ic_professional.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_rating_star.xml b/app/src/main/res/drawable/ic_rating_star.xml new file mode 100644 index 0000000..edb161f --- /dev/null +++ b/app/src/main/res/drawable/ic_rating_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_leading.xml b/app/src/main/res/drawable/ic_search_leading.xml new file mode 100644 index 0000000..8be6984 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_leading.xml @@ -0,0 +1,20 @@ + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d371f8..b2579d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.11.0" -composeBom = "2025.09.01" +composeBom = "2025.11.00" retrofit = "3.0.0" okhttp = "5.1.0" slackComposeLint = "1.4.2" @@ -40,9 +40,9 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3"} okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } slack-compose-lint = { module = "com.slack.lint.compose:compose-lint-checks", version.ref = "slackComposeLint" } From 68431e8c64cce5fa154619ed6e6f7b274ead6d75 Mon Sep 17 00:00:00 2001 From: Andrew Cheung Date: Sun, 16 Nov 2025 03:25:54 -0500 Subject: [PATCH 2/5] added learn destination route --- .../cornellappdev/hustle/ui/navigation/Routes.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt index 286a2c0..23c13bd 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/Routes.kt @@ -5,11 +5,14 @@ import kotlinx.serialization.Serializable sealed interface AppDestination @Serializable -data object Onboarding: AppDestination +data object Onboarding : AppDestination @Serializable data object HomeTab : AppDestination +@Serializable +data object LearnTab : AppDestination + @Serializable data object MessagesTab : AppDestination @@ -21,7 +24,12 @@ sealed interface HomeDestination : AppDestination { data object Home : HomeDestination @Serializable - data class ServiceDetail(val serviceId: String) : HomeDestination + data class ServiceDetail(val serviceId: Int) : HomeDestination +} + +sealed interface LearnDestination : AppDestination { + @Serializable + data object Workshops : LearnDestination } sealed interface MessagesDestination : AppDestination { @@ -37,7 +45,7 @@ sealed interface ProfileDestination : AppDestination { data object EditProfile : ProfileDestination } -sealed interface OnboardingDestination: AppDestination { +sealed interface OnboardingDestination : AppDestination { @Serializable data object SignIn : OnboardingDestination } \ No newline at end of file From cdaed851cc40278391174bffe6e5eae07c2f108b Mon Sep 17 00:00:00 2001 From: Andrew Cheung Date: Sun, 16 Nov 2025 03:35:29 -0500 Subject: [PATCH 3/5] made preview private --- .../cornellappdev/hustle/ui/components/general/HustleButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt index 67d8067..347914a 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt @@ -72,7 +72,7 @@ fun HustleButton( @Preview(showBackground = true) @Composable -fun HustleButtonPreview() { +private fun HustleButtonPreview() { HustleTheme { HustleButton( onClick = {}, From 8a4a15979f26c7da52c578756eebe075f5497ff2 Mon Sep 17 00:00:00 2001 From: Andrew Cheung Date: Sun, 16 Nov 2025 04:01:35 -0500 Subject: [PATCH 4/5] Updated values for carousel to match width and reduce jittering --- .../general/service/ServiceHorizontalCarouselSection.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt index 7d339aa..968cd75 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceHorizontalCarouselSection.kt @@ -35,8 +35,8 @@ fun ServiceHorizontalCarouselSection( header() HorizontalUncontainedCarousel( state = carouselState, - itemWidth = 182.dp, - itemSpacing = HustleSpacing.small, + itemWidth = 186.dp, + itemSpacing = 4.dp, contentPadding = PaddingValues(horizontal = HustleSpacing.large), ) { i -> val service = serviceListings[i] From 942b20524670b5efd47b6c78bef5a2079a67a8e1 Mon Sep 17 00:00:00 2001 From: Andrew Cheung Date: Tue, 18 Nov 2025 22:49:10 -0500 Subject: [PATCH 5/5] Updated parameter orders, dependency versions, formatting --- .../hustle/ui/components/general/HustleButton.kt | 2 +- .../components/general/service/FavoriteButton.kt | 1 - .../ui/components/general/service/ServiceCard.kt | 4 ++-- .../general/service/ServiceRatingLabel.kt | 3 ++- .../hustle/util/constants/HustleConstants.kt | 4 ++-- gradle/libs.versions.toml | 16 ++++++++-------- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt index 347914a..c0741b5 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/HustleButton.kt @@ -28,8 +28,8 @@ import com.cornellappdev.hustle.ui.theme.HustleTheme fun HustleButton( onClick: () -> Unit, text: String?, - textStyle: TextStyle = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), enabled: Boolean = true, shape: Shape = RoundedCornerShape(20.dp), border: BorderStroke? = null, diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt index a0eac1e..04ed846 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/FavoriteButton.kt @@ -45,7 +45,6 @@ fun FavoriteButton( onClick = { if (!isFavorite) { coroutineScope.launch { - // Animate the button to scale up and then back to normal size scale.animateTo(1.2f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) scale.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) } diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt index b8ba5ff..ad0930c 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceCard.kt @@ -52,10 +52,10 @@ fun ServiceCard( priceUnit: String, serviceRating: Double, onClick: () -> Unit, + modifier: Modifier = Modifier, isFavorite: Boolean? = null, onFavoriteClick: (() -> Unit)? = null, - cardType: ServiceCardType = ServiceCardType.DEFAULT, - modifier: Modifier = Modifier + cardType: ServiceCardType = ServiceCardType.DEFAULT ) { val cardHeight = when (cardType) { ServiceCardType.DEFAULT -> 279.dp diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt index a0c6946..4071d93 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/general/service/ServiceRatingLabel.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.hustle.R import com.cornellappdev.hustle.ui.theme.HustleTheme +import java.util.Locale @Composable fun ServiceRatingLabel( @@ -33,7 +34,7 @@ fun ServiceRatingLabel( tint = Color.Unspecified ) Text( - text = String.format("%.1f", rating), + text = String.format(Locale.US, "%.1f", rating), style = textStyle ) } diff --git a/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt b/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt index 831de33..84eda21 100644 --- a/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt +++ b/app/src/main/java/com/cornellappdev/hustle/util/constants/HustleConstants.kt @@ -4,8 +4,8 @@ import androidx.annotation.DrawableRes import com.cornellappdev.hustle.R data class ServiceCategory( - val name: String, @DrawableRes - val iconResId: Int + val name: String, + @DrawableRes val iconResId: Int ) val SERVICE_CATEGORIES = listOf( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2579d9..5ad7b34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "8.13.0" -hiltAndroid = "2.57.1" +agp = "8.13.1" +hiltAndroid = "2.57.2" hiltNavigationCompose = "1.3.0" -kotlin = "2.2.20" +kotlin = "2.2.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" @@ -11,16 +11,16 @@ lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.11.0" composeBom = "2025.11.00" retrofit = "3.0.0" -okhttp = "5.1.0" +okhttp = "5.3.2" slackComposeLint = "1.4.2" kotlinxSerialization = "1.9.0" -composeNavigation = "2.9.5" -firebaseBom = "34.3.0" +composeNavigation = "2.9.6" +firebaseBom = "34.6.0" credentialManager = "1.5.0" googleId = "1.1.1" -google-services = "4.4.3" +google-services = "4.4.4" coil = "3.3.0" -splashScreen = "1.0.1" +splashScreen = "1.2.0" dataStorePreferences = "1.1.7" [libraries]