diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/home/CategoryButtonRow.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/CategoryButtonRow.kt new file mode 100644 index 0000000..11512ec --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/CategoryButtonRow.kt @@ -0,0 +1,53 @@ +package com.cornellappdev.hustle.ui.components.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.cornellappdev.hustle.ui.components.general.HustleButton +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.util.constants.CategoryType +import com.cornellappdev.hustle.util.constants.SERVICE_CATEGORIES + +@Composable +fun CategoryButtonRow( + onCategoryClick: (CategoryType) -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(horizontal = HustleSpacing.large) +) { + val lazyListState = rememberLazyListState() + LazyRow( + state = lazyListState, + modifier = modifier, + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(HustleSpacing.extraSmall) + ) { + items(SERVICE_CATEGORIES, key = { it.categoryType }) { category -> + val categoryType = category.categoryType + HustleButton( + onClick = { onCategoryClick(categoryType) }, + text = categoryType.typeName, + leadingIcon = { + Icon( + painter = painterResource(id = category.iconResId), + contentDescription = categoryType.typeName, + ) + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CategoryButtonRowPreview() { + CategoryButtonRow( + onCategoryClick = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/home/MainContent.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/MainContent.kt new file mode 100644 index 0000000..e0c722e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/MainContent.kt @@ -0,0 +1,126 @@ +package com.cornellappdev.hustle.ui.components.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +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.components.general.service.ServiceHorizontalCarouselSection +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme +import com.cornellappdev.hustle.util.constants.CategoryType +import com.cornellappdev.hustle.util.constants.TEST_SERVICES + +@Composable +fun MainContent( + popularRightNowListings: List, + newOnHustleListings: List, + servicesNearYouListings: List, + availableThisWeekListings: List, + navigateToCategorySubpage: (CategoryType) -> Unit, + navigateToServiceDetail: (Int) -> Unit, + onFavoriteClick: (Int) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(top = HustleSpacing.small), + ) { + item { + CategoryButtonRow(onCategoryClick = navigateToCategorySubpage) + } + item { + Spacer(modifier = Modifier.height(HustleSpacing.medium)) + } + item { + Column( + verticalArrangement = Arrangement.spacedBy(HustleSpacing.large) + ) { + ServiceHorizontalCarouselSection( + serviceListings = popularRightNowListings, + onServiceClick = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick, + header = { + ClickableSectionHeader( + title = CategoryType.POPULAR_RIGHT_NOW.typeName, + onClick = { + navigateToCategorySubpage(CategoryType.POPULAR_RIGHT_NOW) + }, + modifier = Modifier.padding(start = 32.dp) + ) + } + ) + + ServiceHorizontalCarouselSection( + serviceListings = newOnHustleListings, + onServiceClick = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick, + header = { + ClickableSectionHeader( + title = CategoryType.NEW_ON_HUSTLE.typeName, + onClick = { + navigateToCategorySubpage(CategoryType.NEW_ON_HUSTLE) + }, + modifier = Modifier.padding(start = 32.dp) + ) + } + ) + + ServiceHorizontalCarouselSection( + serviceListings = servicesNearYouListings, + onServiceClick = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick, + header = { + ClickableSectionHeader( + title = CategoryType.SERVICES_NEAR_YOU.typeName, + onClick = { + navigateToCategorySubpage(CategoryType.SERVICES_NEAR_YOU) + }, + modifier = Modifier.padding(start = 32.dp) + ) + } + ) + + ServiceHorizontalCarouselSection( + serviceListings = availableThisWeekListings, + onServiceClick = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick, + header = { + ClickableSectionHeader( + title = CategoryType.AVAILABLE_THIS_WEEK.typeName, + onClick = { + navigateToCategorySubpage(CategoryType.AVAILABLE_THIS_WEEK) + }, + modifier = Modifier.padding(start = 32.dp) + ) + } + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MainContentPreview() { + HustleTheme { + MainContent( + popularRightNowListings = TEST_SERVICES, + newOnHustleListings = TEST_SERVICES, + servicesNearYouListings = TEST_SERVICES, + availableThisWeekListings = TEST_SERVICES, + navigateToCategorySubpage = {}, + navigateToServiceDetail = {}, + onFavoriteClick = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchContent.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchContent.kt new file mode 100644 index 0000000..d93df02 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchContent.kt @@ -0,0 +1,153 @@ +package com.cornellappdev.hustle.ui.components.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.R +import com.cornellappdev.hustle.data.model.services.Service +import com.cornellappdev.hustle.ui.components.general.service.ServiceHorizontalCarouselSection +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_RECENT_SEARCHES +import com.cornellappdev.hustle.util.constants.TEST_SERVICES + +@Composable +fun SearchContent( + recentSearches: List, + recentlyViewedServices: List, + onSearchSuggestionClick: (String) -> Unit, + navigateToServiceDetail: (Int) -> Unit, + onFavoriteClick: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(top = 4.dp) + .verticalScroll(rememberScrollState()), + ) { + AnimatedVisibility( + visible = recentSearches.isNotEmpty(), + label = "Recent Searches Visibility", + enter = fadeIn(animationSpec = tween(300)), + exit = fadeOut(animationSpec = tween(300)) + ) { + RecentSearchesSection(recentSearches, onSearchSuggestionClick) + } + + Spacer(modifier = Modifier.height(44.dp)) + + AnimatedVisibility( + visible = recentlyViewedServices.isNotEmpty(), + label = "Recently Viewed Services Visibility", + enter = fadeIn(animationSpec = tween(300)), + exit = fadeOut(animationSpec = tween(300)) + ) { + ServiceHorizontalCarouselSection( + serviceListings = recentlyViewedServices, + onServiceClick = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick, + header = { + Text( + text = "Recently viewed", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(start = HustleSpacing.large) + ) + } + ) + } + } +} + +@Composable +private fun RecentSearchesSection( + recentSearches: List, + onSearchSuggestionClick: (String) -> Unit +) { + Column( + modifier = Modifier.padding(horizontal = HustleSpacing.large) + ) { + Text( + text = "Recent", + style = MaterialTheme.typography.headlineSmall + ) + + recentSearches.forEach { recentSearch -> + RecentSearchItem( + recentSearch = recentSearch, + onSearchSuggestionClick = onSearchSuggestionClick + ) + } + } +} + +@Composable +private fun RecentSearchItem( + recentSearch: String, + onSearchSuggestionClick: (String) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(HustleSpacing.small), + modifier = Modifier + .clickable( + onClick = { + onSearchSuggestionClick(recentSearch) + } + ) + .padding(top = HustleSpacing.medium) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + + ) { + Icon( + painter = painterResource(R.drawable.ic_history), + contentDescription = "Recent Search Icon", + tint = Color.Unspecified + ) + Text( + text = recentSearch, + style = MaterialTheme.typography.labelLarge + ) + } + HorizontalDivider(color = HustleColors.iconInactive) + } +} + +@Preview(showBackground = true) +@Composable +private fun SearchContentPreview() { + HustleTheme { + SearchContent( + recentSearches = TEST_RECENT_SEARCHES, + recentlyViewedServices = TEST_SERVICES, + onSearchSuggestionClick = {}, + navigateToServiceDetail = {}, + onFavoriteClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchHeader.kt b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchHeader.kt new file mode 100644 index 0000000..8d72dd6 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/hustle/ui/components/home/SearchHeader.kt @@ -0,0 +1,143 @@ +package com.cornellappdev.hustle.ui.components.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +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.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.size +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.hustle.ui.components.general.BackButton +import com.cornellappdev.hustle.ui.components.general.HustleSearchBar +import com.cornellappdev.hustle.ui.theme.HustleColors +import com.cornellappdev.hustle.ui.theme.HustleSpacing + +@Composable +fun SearchHeader( + queryState: TextFieldState, + isSearchActive: Boolean, + onSearchActiveChange: (Boolean) -> Unit, + onSearch: () -> Unit, + modifier: Modifier = Modifier +) { + val focusManager = LocalFocusManager.current + val animationProgress by animateFloatAsState( + targetValue = if (isSearchActive) 1.0f else 0.0f, + animationSpec = tween(durationMillis = 300), + label = "SearchHeaderAnimation" + ) + var headerHeightPx by remember { mutableIntStateOf(0) } + var gapHeightPx by remember { mutableIntStateOf(0) } + + Column(modifier = modifier) { + Text( + text = "Hustle", + style = MaterialTheme.typography.headlineMedium.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold, + color = HustleColors.hustleGreen + ), + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { headerHeightPx = it.height } + .graphicsLayer { + translationY = animationProgress * -headerHeightPx + alpha = 1.0f - animationProgress + } + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(HustleSpacing.extraSmall) + .onSizeChanged { gapHeightPx = it.height } + ) + SearchBarRow( + isSearchActive, + queryState, + onBackClick = { + onSearchActiveChange(false) + focusManager.clearFocus() + }, + onFocus = { onSearchActiveChange(true) }, + onSearch = onSearch, + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { + val totalDisplacement = headerHeightPx + gapHeightPx + translationY = animationProgress * -totalDisplacement + } + ) + } +} + +@Composable +private fun SearchBarRow( + isSearchActive: Boolean, + queryState: TextFieldState, + onBackClick: () -> Unit, + onFocus: () -> Unit, + onSearch: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(HustleSpacing.medium), + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility( + visible = isSearchActive, + enter = fadeIn() + scaleIn(), + exit = fadeOut(animationSpec = tween(150)) + shrinkHorizontally( + shrinkTowards = Alignment.Start, + animationSpec = tween(150) + ) + ) { + BackButton(onClick = onBackClick, modifier = Modifier.size(12.dp)) + } + HustleSearchBar( + queryState = queryState, + isSearchActive = isSearchActive, + onFocus = onFocus, + onSearch = onSearch, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SearchHeaderPreview() { + var isSearchActive by remember { mutableStateOf(false) } + val queryState = rememberTextFieldState() + SearchHeader( + queryState = queryState, + isSearchActive = isSearchActive, + onSearchActiveChange = { isSearchActive = it }, + onSearch = {} + ) +} \ No newline at end of file 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 7ecd607..f99dc94 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 @@ -20,7 +20,6 @@ fun HustleNavigation( ) { val navController = rememberNavController() val startDestination = if (isSignedIn) HomeTab else Onboarding - val currentRoute = navController.currentBackStackEntry?.destination?.route LaunchedEffect(isSignedIn) { if (!isSignedIn && navController.currentDestination != Onboarding) { 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 23c13bd..9703423 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 @@ -1,5 +1,6 @@ package com.cornellappdev.hustle.ui.navigation +import com.cornellappdev.hustle.util.constants.CategoryType import kotlinx.serialization.Serializable sealed interface AppDestination @@ -25,6 +26,9 @@ sealed interface HomeDestination : AppDestination { @Serializable data class ServiceDetail(val serviceId: Int) : HomeDestination + + @Serializable + data class CategoryServices(val categoryType: CategoryType) : HomeDestination } sealed interface LearnDestination : AppDestination { diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/HomeNavigation.kt b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/HomeNavigation.kt index c6fea98..43ed19f 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/HomeNavigation.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/navigation/navgraphs/HomeNavigation.kt @@ -12,11 +12,20 @@ import com.cornellappdev.hustle.ui.screens.home.HomeScreen fun NavGraphBuilder.homeNavGraph(navController: NavHostController) { navigation(startDestination = HomeDestination.Home) { composable { - HomeScreen() + HomeScreen( + navigateToServiceDetail = { serviceId -> + navController.navigate(HomeDestination.ServiceDetail(serviceId)) + }, + navigateToCategorySubpage = { categoryType -> + navController.navigate(HomeDestination.CategoryServices(categoryType)) + } + ) } composable { backStackEntry -> val serviceId = backStackEntry.toRoute() } + + composable {} } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt index 082f1f4..7414358 100644 --- a/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/hustle/ui/screens/home/HomeScreen.kt @@ -1,11 +1,162 @@ package com.cornellappdev.hustle.ui.screens.home -import androidx.compose.material3.Text +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState 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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.cornellappdev.hustle.data.model.services.Service +import com.cornellappdev.hustle.ui.components.home.MainContent +import com.cornellappdev.hustle.ui.components.home.SearchContent +import com.cornellappdev.hustle.ui.components.home.SearchHeader +import com.cornellappdev.hustle.ui.theme.HustleSpacing +import com.cornellappdev.hustle.ui.theme.HustleTheme import com.cornellappdev.hustle.ui.viewmodels.home.HomeScreenViewModel +import com.cornellappdev.hustle.util.constants.CategoryType +import com.cornellappdev.hustle.util.constants.TEST_RECENT_SEARCHES +import com.cornellappdev.hustle.util.constants.TEST_SERVICES @Composable -fun HomeScreen(viewModel: HomeScreenViewModel = hiltViewModel()) { - Text("Home Screen") +fun HomeScreen( + navigateToCategorySubpage: (CategoryType) -> Unit, + navigateToServiceDetail: (Int) -> Unit, + viewModel: HomeScreenViewModel = hiltViewModel() +) { + // TODO: Add states to viewmodel and replace test data with real data from viewmodel + val queryState = rememberTextFieldState() + var isSearchActive by remember { mutableStateOf(false) } + val popularRightNowListings = TEST_SERVICES + val newOnHustleListings = TEST_SERVICES + val servicesNearYouListings = TEST_SERVICES + val availableThisWeekListings = TEST_SERVICES + val recentSearches = TEST_RECENT_SEARCHES + val recentlyViewedServiceListings = TEST_SERVICES + + HomeScreenContent( + homeScreenViewState = HomeScreenViewState( + queryState = queryState, + isSearchActive = isSearchActive, + popularRightNowListings = popularRightNowListings, + newOnHustleListings = newOnHustleListings, + servicesNearYouListings = servicesNearYouListings, + availableThisWeekListings = availableThisWeekListings, + recentSearches = recentSearches, + recentlyViewedServices = recentlyViewedServiceListings + ), + onSearchActiveChange = { isActive -> isSearchActive = isActive }, + onSearch = {}, + onSearchSuggestionClick = {}, + navigateToCategorySubpage = navigateToCategorySubpage, + navigateToServiceDetail = navigateToServiceDetail, + onFavoriteClick = {} + ) +} + +data class HomeScreenViewState( + val queryState: TextFieldState, + val isSearchActive: Boolean, + val popularRightNowListings: List, + val newOnHustleListings: List, + val servicesNearYouListings: List, + val availableThisWeekListings: List, + val recentSearches: List, + val recentlyViewedServices: List, +) + +@Composable +private fun HomeScreenContent( + homeScreenViewState: HomeScreenViewState, + onSearchActiveChange: (Boolean) -> Unit, + onSearch: () -> Unit, + onSearchSuggestionClick: (String) -> Unit, + navigateToCategorySubpage: (CategoryType) -> Unit, + navigateToServiceDetail: (Int) -> Unit, + onFavoriteClick: (Int) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + SearchHeader( + queryState = homeScreenViewState.queryState, + isSearchActive = homeScreenViewState.isSearchActive, + onSearchActiveChange = onSearchActiveChange, + onSearch = onSearch, + modifier = Modifier.padding(horizontal = HustleSpacing.large) + ) + AnimatedContent( + targetState = homeScreenViewState.isSearchActive, + label = "HomeScreenContentAnimation", + transitionSpec = { + fadeIn(animationSpec = tween(300)) togetherWith fadeOut( + animationSpec = tween( + 300 + ) + ) + }, + ) { isSearchActive -> + if (!isSearchActive) { + MainContent( + popularRightNowListings = homeScreenViewState.popularRightNowListings, + newOnHustleListings = homeScreenViewState.newOnHustleListings, + servicesNearYouListings = homeScreenViewState.servicesNearYouListings, + availableThisWeekListings = homeScreenViewState.availableThisWeekListings, + navigateToCategorySubpage = navigateToCategorySubpage, + navigateToServiceDetail = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick + ) + } else { + SearchContent( + recentSearches = homeScreenViewState.recentSearches, + recentlyViewedServices = homeScreenViewState.recentlyViewedServices, + onSearchSuggestionClick = onSearchSuggestionClick, + navigateToServiceDetail = navigateToServiceDetail, + onFavoriteClick = onFavoriteClick + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HomeScreenPreview() { + val queryState = rememberTextFieldState() + var isSearchActive by remember { mutableStateOf(false) } + HustleTheme { + HomeScreenContent( + homeScreenViewState = HomeScreenViewState( + queryState = queryState, + isSearchActive = isSearchActive, + popularRightNowListings = TEST_SERVICES, + newOnHustleListings = TEST_SERVICES, + servicesNearYouListings = TEST_SERVICES, + availableThisWeekListings = TEST_SERVICES, + recentSearches = TEST_RECENT_SEARCHES, + recentlyViewedServices = TEST_SERVICES + ), + onSearchActiveChange = { isActive -> isSearchActive = isActive }, + onSearch = {}, + onSearchSuggestionClick = {}, + navigateToCategorySubpage = {}, + navigateToServiceDetail = {}, + onFavoriteClick = {} + ) + } } \ No newline at end of file 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 84eda21..4bc2d17 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 @@ -2,15 +2,28 @@ package com.cornellappdev.hustle.util.constants import androidx.annotation.DrawableRes import com.cornellappdev.hustle.R +import kotlinx.serialization.Serializable +//TODO: Separate into different constant files or data package if necessary +@Serializable +enum class CategoryType(val typeName: String) { + LESSONS("Lessons"), + PHOTO("Photo"), + BEAUTY("Beauty"), + PROFESSIONAL("Professional"), + POPULAR_RIGHT_NOW("Popular right now"), + NEW_ON_HUSTLE("New on Hustle"), + SERVICES_NEAR_YOU("Services near you"), + AVAILABLE_THIS_WEEK("Available this week"); +} data class ServiceCategory( - val name: String, + val categoryType: CategoryType, @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) + ServiceCategory(CategoryType.LESSONS, R.drawable.ic_lessons), + ServiceCategory(CategoryType.PHOTO, R.drawable.ic_photo), + ServiceCategory(CategoryType.BEAUTY, R.drawable.ic_beauty), + ServiceCategory(CategoryType.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 index 5ae0e66..4f7241f 100644 --- a/app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/hustle/util/constants/TestingConstants.kt @@ -35,57 +35,56 @@ val TEST_SERVICES = listOf( ), Service( id = 2, - name = "Dreamy fall grad photo session", - category = "Photo", - minimumPrice = 67.0, - rating = 4.1, + name = "Pretty Nail Painting Service", + category = "Beauty", + minimumPrice = 10.50, + rating = 4.5, isFavorited = false, user = User( firebaseUid = "", email = "", - displayName = "Jane Doe", + displayName = "Lauren Ah-Hot", 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", + displayImageUrl = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQhJqCvqqrdsOc9pS4HZiHvoA-gtow5VeUF0g&s", priceUnit = "/hour", ), Service( id = 3, - name = "Dreamy fall grad photo session", - category = "Photo", - minimumPrice = 67.0, - rating = 4.1, + name = "Cheap Haircuts for Males", + category = "Beauty", + minimumPrice = 20.0, + rating = 4.2, isFavorited = false, user = User( firebaseUid = "", email = "", - displayName = "Jane Doe", + displayName = "Joshua Dirga", 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", + displayImageUrl = "https://s3-media0.fl.yelpcdn.com/bphoto/1KwtwltxdEYVz4TIHAzaow/1000s.jpg" ), Service( id = 4, - name = "Dreamy fall grad photo session", - category = "Photo", - minimumPrice = 67.0, - rating = 4.1, + name = "Computer Science Tutoring", + category = "Lessons", + minimumPrice = 15.0, + rating = 3.5, isFavorited = false, user = User( firebaseUid = "", email = "", - displayName = "Jane Doe", + displayName = "Andrew Cheung", 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", + displayImageUrl = "https://www.engineering.cornell.edu/wp-content/uploads/2025/03/Duffield-Atrium-Students-Header-02.jpg", priceUnit = "/hour", ), Service( id = 5, - name = "Dreamy fall grad photo session", - category = "Photo", - minimumPrice = 67.0, + name = "Really Awesome Resume Review and Editing Session", + category = "Professional", + minimumPrice = 35.0, rating = 4.1, isFavorited = false, user = User( @@ -94,7 +93,7 @@ val TEST_SERVICES = listOf( 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", + displayImageUrl = "https://www.engineering.cornell.edu/wp-content/uploads/2024/10/AU-CornellEngineering-March082023-181.jpg", priceUnit = "/hour", ),