Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.navigation.CategoryType
import com.cornellappdev.hustle.ui.theme.HustleSpacing
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.name }) { category ->
HustleButton(
onClick = { onCategoryClick(CategoryType.fromTypeName(category.name)) },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we just directly pass in category to onCategoryClick, removing the need for the fromTypeName function entirely?

Copy link
Copy Markdown
Member Author

@AndrewCheung360 AndrewCheung360 Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning on moving the categorytype definition to HustleConstants and replacing the ServiceCategory type's name field with categoryType: CategoryType, so that should remove the need for the extra function

text = category.name,
leadingIcon = {
Icon(
painter = painterResource(id = category.iconResId),
contentDescription = category.name,
)
}
)
}
}
}

@Preview(showBackground = true)
@Composable
private fun CategoryButtonRowPreview() {
CategoryButtonRow(
onCategoryClick = {}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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.navigation.CategoryType
import com.cornellappdev.hustle.ui.theme.HustleSpacing
import com.cornellappdev.hustle.ui.theme.HustleTheme
import com.cornellappdev.hustle.util.constants.TEST_SERVICES

@Composable
fun MainContent(
popularRightNowListings: List<Service>,
newOnHustleListings: List<Service>,
servicesNearYouListings: List<Service>,
availableThisWeekListings: List<Service>,
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)
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded padding value 32.dp should use HustleSpacing.large (which is 24.dp) or another appropriate spacing constant for consistency. This value appears on lines 58, 73, 88, and 103.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good thought, but would need to consult design

)
}
)

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() {
val popularRightNowListings = TEST_SERVICES
val newOnHustleListings = TEST_SERVICES
val servicesNearYouListings = TEST_SERVICES
val availableThisWeekListings = TEST_SERVICES
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we just pass in TEST_SERVICES directly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, I kinda forgot about that.

HustleTheme {
MainContent(
popularRightNowListings = popularRightNowListings,
newOnHustleListings = newOnHustleListings,
servicesNearYouListings = servicesNearYouListings,
availableThisWeekListings = availableThisWeekListings,
navigateToCategorySubpage = {},
navigateToServiceDetail = {},
onFavoriteClick = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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<String>,
recentlyViewedServices: List<Service>,
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<String>,
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() {
val recentSearches = TEST_RECENT_SEARCHES
val recentlyViewedServiceListings = TEST_SERVICES
HustleTheme {
SearchContent(
recentSearches = recentSearches,
recentlyViewedServices = recentlyViewedServiceListings,
onSearchSuggestionClick = {},
navigateToServiceDetail = {},
onFavoriteClick = {}
)
}
}
Loading
Loading