diff --git a/app/src/main/java/com/hieuwu/groceriesstore/data/network/ApiService.kt b/app/src/main/java/com/hieuwu/groceriesstore/data/network/ApiService.kt index 8aa02d30..cade4d3f 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/data/network/ApiService.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/data/network/ApiService.kt @@ -23,9 +23,6 @@ private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build() -var rapidApiHost = "tasty.p.rapidapi.com" -var rapidApiKey = "25623d5122mshdf7d5b25fc85d92p16d42bjsn846c1ccf5e93" - interface RecipesApiService { @Headers( "x-rapidapi-host: tasty.p.rapidapi.com", diff --git a/app/src/main/java/com/hieuwu/groceriesstore/data/network/dto/Meal.kt b/app/src/main/java/com/hieuwu/groceriesstore/data/network/dto/Meal.kt new file mode 100644 index 00000000..67d16c47 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/data/network/dto/Meal.kt @@ -0,0 +1,25 @@ +package com.hieuwu.groceriesstore.data.network.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Meal( + @SerialName("id") + val id: String, + + @SerialName("name") + val name: String, + + @SerialName("ingredients") + val ingredients: List, + + @SerialName("week_day") + val weekDay: String, + + @SerialName("creator") + val creatorId: String, + + @SerialName("meal_type") + val mealType: String, +) diff --git a/app/src/main/java/com/hieuwu/groceriesstore/data/repository/MealPlanRepository.kt b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/MealPlanRepository.kt new file mode 100644 index 00000000..de448cb2 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/MealPlanRepository.kt @@ -0,0 +1,15 @@ +package com.hieuwu.groceriesstore.data.repository + +import com.hieuwu.groceriesstore.domain.models.MealModel + +interface MealPlanRepository { + suspend fun addMealToPlan( + weekDay: String, + name: String, + ingredients: List, + mealType: String + ) + + suspend fun retrieveMealByType(type: String, weekDayValue: String): List + suspend fun removeMealFromPlan(id: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/MealPlanRepositoryImpl.kt b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/MealPlanRepositoryImpl.kt new file mode 100644 index 00000000..1fdc64ee --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/MealPlanRepositoryImpl.kt @@ -0,0 +1,66 @@ +package com.hieuwu.groceriesstore.data.repository.impl + +import com.hieuwu.groceriesstore.data.network.dto.Meal +import com.hieuwu.groceriesstore.data.network.dto.ProductDto +import com.hieuwu.groceriesstore.data.repository.MealPlanRepository +import com.hieuwu.groceriesstore.data.repository.UserRepository +import com.hieuwu.groceriesstore.domain.models.MealModel +import io.github.jan.supabase.postgrest.Postgrest +import java.util.UUID +import kotlinx.coroutines.flow.first +import timber.log.Timber +import javax.inject.Inject + +class MealPlanRepositoryImpl @Inject constructor( + private val postgrest: Postgrest, + private val userRepository: UserRepository +) : MealPlanRepository { + override suspend fun addMealToPlan( + weekDay: String, + name: String, + ingredients: List, + mealType: String + ) { + val userId = userRepository.getCurrentUser().first()?.id ?: "" + postgrest["meal_plans"].insert( + Meal( + id = UUID.randomUUID().toString(), + name = name, + ingredients = ingredients, + creatorId = userId, + weekDay = weekDay, + mealType = mealType, + ) + ) + } + + override suspend fun retrieveMealByType(type: String, weekDayValue: String): List { + val userId = userRepository.getCurrentUser().first()?.id ?: "" + val result = postgrest["meal_plans"].select { + filter { + eq("creator", userId) + eq("week_day", weekDayValue) + eq("meal_type", type) + } + }.decodeList().map { + Timber.d(it.toString()) + MealModel( + id = it.id, + name = it.name, + ingredients = it.ingredients, + weekDay = it.weekDay, + creatorId = it.creatorId, + mealType = it.mealType + ) + } + return result + } + + override suspend fun removeMealFromPlan(id: String) { + postgrest["meal_plans"].delete { + filter { + eq("id", id) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/UserRepositoryImpl.kt b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/UserRepositoryImpl.kt index 738b33c9..4111f5f2 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/UserRepositoryImpl.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/data/repository/impl/UserRepositoryImpl.kt @@ -54,7 +54,13 @@ class UserRepositoryImpl @Inject constructor( this.password = password } - val userDto = postgrest[CollectionNames.users].select().decodeSingle() + val userDto = postgrest[CollectionNames.users].select() + { + filter { + UserDto::email eq email + } + } + .decodeSingle() val user = SupabaseMapper.mapDtoToEntity(userDto) userDao.insert(user) true diff --git a/app/src/main/java/com/hieuwu/groceriesstore/di/RepositoryModule.kt b/app/src/main/java/com/hieuwu/groceriesstore/di/RepositoryModule.kt index 9c993cd7..5ace274f 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/di/RepositoryModule.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/di/RepositoryModule.kt @@ -6,10 +6,12 @@ import com.hieuwu.groceriesstore.data.repository.impl.ProductRepositoryImpl import com.hieuwu.groceriesstore.data.repository.impl.RecipeRepositoryImpl import com.hieuwu.groceriesstore.data.repository.impl.UserRepositoryImpl import com.hieuwu.groceriesstore.data.repository.CategoryRepository +import com.hieuwu.groceriesstore.data.repository.MealPlanRepository import com.hieuwu.groceriesstore.data.repository.OrderRepository import com.hieuwu.groceriesstore.data.repository.ProductRepository import com.hieuwu.groceriesstore.data.repository.RecipeRepository import com.hieuwu.groceriesstore.data.repository.UserRepository +import com.hieuwu.groceriesstore.data.repository.impl.MealPlanRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -39,4 +41,8 @@ abstract class RepositoryModule { @Singleton @Binds abstract fun bindRecipeRepository(impl: RecipeRepositoryImpl): RecipeRepository + + @Singleton + @Binds + abstract fun bindMealRepository(impl: MealPlanRepositoryImpl): MealPlanRepository } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/di/SupabaseModule.kt b/app/src/main/java/com/hieuwu/groceriesstore/di/SupabaseModule.kt index 5fa23f4a..c0e13101 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/di/SupabaseModule.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/di/SupabaseModule.kt @@ -11,7 +11,9 @@ import io.github.jan.supabase.gotrue.Auth import io.github.jan.supabase.gotrue.auth import io.github.jan.supabase.postgrest.Postgrest import io.github.jan.supabase.postgrest.postgrest +import io.github.jan.supabase.serializer.KotlinXSerializer import io.ktor.client.plugins.* +import kotlinx.serialization.json.Json import javax.inject.Singleton @InstallIn(SingletonComponent::class) @@ -23,8 +25,9 @@ object SupabaseModule { fun provideSupabaseClient(): SupabaseClient { return createSupabaseClient( supabaseUrl = BuildConfig.SUPABASE_URL, - supabaseKey = BuildConfig.API_KEY + supabaseKey = BuildConfig.API_KEY, ) { + defaultSerializer = KotlinXSerializer(Json { ignoreUnknownKeys = true }) install(Postgrest) install(Auth) } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/di/UseCaseModule.kt b/app/src/main/java/com/hieuwu/groceriesstore/di/UseCaseModule.kt index dd1b6847..8fab1d70 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/di/UseCaseModule.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/di/UseCaseModule.kt @@ -1,5 +1,6 @@ package com.hieuwu.groceriesstore.di +import com.hieuwu.groceriesstore.domain.usecases.AddMealToPlanUseCase import com.hieuwu.groceriesstore.domain.usecases.AddToCartUseCase import com.hieuwu.groceriesstore.domain.usecases.impl.AddToCartUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.CreateNewOrderUseCase @@ -18,6 +19,8 @@ import com.hieuwu.groceriesstore.domain.usecases.impl.GetProductsListUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.GetProfileUseCase import com.hieuwu.groceriesstore.domain.usecases.impl.GetProfileUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.RefreshAppDataUseCase +import com.hieuwu.groceriesstore.domain.usecases.RemoveMealFromPlanUseCase +import com.hieuwu.groceriesstore.domain.usecases.RetrieveMealByTypeUseCase import com.hieuwu.groceriesstore.domain.usecases.impl.RefreshAppDataUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.SearchProductUseCase import com.hieuwu.groceriesstore.domain.usecases.impl.SearchProductUseCaseImpl @@ -32,7 +35,10 @@ import com.hieuwu.groceriesstore.domain.usecases.impl.UpdateCartItemUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.UpdateProfileUseCase import com.hieuwu.groceriesstore.domain.usecases.impl.UpdateProfileUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.UserSettingsUseCase +import com.hieuwu.groceriesstore.domain.usecases.impl.AddMealToPlanUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.impl.GetOrderListUseCaseImpl +import com.hieuwu.groceriesstore.domain.usecases.impl.RemoveMealFromPlanUseCaseImpl +import com.hieuwu.groceriesstore.domain.usecases.impl.RetrieveMealByTypeUseCaseImpl import com.hieuwu.groceriesstore.domain.usecases.impl.UserSettingsUseCaseImpl import dagger.Binds import dagger.Module @@ -111,4 +117,17 @@ abstract class UseCaseModule { @Binds abstract fun bindGetOrdersUseCase(impl: GetOrderListUseCaseImpl): GetOrderListUseCase + @ViewModelScoped + @Binds + abstract fun bindAddMealToPlan(impl: AddMealToPlanUseCaseImpl): AddMealToPlanUseCase + + @ViewModelScoped + @Binds + abstract fun bindRetrieveMealByType(impl: RetrieveMealByTypeUseCaseImpl): RetrieveMealByTypeUseCase + + + @ViewModelScoped + @Binds + abstract fun bindRemoveMealFromPlan(impl: RemoveMealFromPlanUseCaseImpl): RemoveMealFromPlanUseCase + } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/models/MealModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/models/MealModel.kt new file mode 100644 index 00000000..7050eaa0 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/models/MealModel.kt @@ -0,0 +1,10 @@ +package com.hieuwu.groceriesstore.domain.models + +data class MealModel( + val id: String, + val name: String, + val ingredients: List, + val weekDay: String, + val creatorId: String, + val mealType: String, +) diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddMealToPlanUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddMealToPlanUseCase.kt new file mode 100644 index 00000000..bff49405 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddMealToPlanUseCase.kt @@ -0,0 +1,11 @@ +package com.hieuwu.groceriesstore.domain.usecases + +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.MealType + +interface AddMealToPlanUseCase : + SuspendUseCase { + class Input(val name: String, val weekDay: String, val ingredients: List, + val mealType: MealType + ) + object Output +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RemoveMealFromPlanUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RemoveMealFromPlanUseCase.kt new file mode 100644 index 00000000..896d1c17 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RemoveMealFromPlanUseCase.kt @@ -0,0 +1,10 @@ +package com.hieuwu.groceriesstore.domain.usecases + +interface RemoveMealFromPlanUseCase : + SuspendUseCase { + class Input(val id: String) + sealed class Output { + object Success: Output() + object Failure: Output() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RetrieveMealByTypeUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RetrieveMealByTypeUseCase.kt new file mode 100644 index 00000000..66713c65 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RetrieveMealByTypeUseCase.kt @@ -0,0 +1,18 @@ +package com.hieuwu.groceriesstore.domain.usecases + +import com.hieuwu.groceriesstore.domain.models.MealModel +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.MealType +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.WeekDayValue + +interface RetrieveMealByTypeUseCase : + SuspendUseCase { + class Input( + val dayValue: WeekDayValue, + val mealType: MealType, + ) + + sealed class Output { + class Success(val data: List) : Output() + data object Failure : Output() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/AddMealToPlanUseCaseImpl.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/AddMealToPlanUseCaseImpl.kt new file mode 100644 index 00000000..1c3bb178 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/AddMealToPlanUseCaseImpl.kt @@ -0,0 +1,19 @@ +package com.hieuwu.groceriesstore.domain.usecases.impl + +import com.hieuwu.groceriesstore.data.repository.MealPlanRepository +import com.hieuwu.groceriesstore.domain.usecases.AddMealToPlanUseCase +import javax.inject.Inject + +class AddMealToPlanUseCaseImpl @Inject constructor( + private val mealPlanRepository: MealPlanRepository +) : AddMealToPlanUseCase { + override suspend fun execute(input: AddMealToPlanUseCase.Input): AddMealToPlanUseCase.Output { + mealPlanRepository.addMealToPlan( + weekDay = input.weekDay, + name = input.name, + ingredients = input.ingredients, + mealType = input.mealType.value + ) + return AddMealToPlanUseCase.Output + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RemoveMealFromPlanUseCaseImpl.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RemoveMealFromPlanUseCaseImpl.kt new file mode 100644 index 00000000..fae7433f --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RemoveMealFromPlanUseCaseImpl.kt @@ -0,0 +1,24 @@ +package com.hieuwu.groceriesstore.domain.usecases.impl + +import com.hieuwu.groceriesstore.data.repository.MealPlanRepository +import com.hieuwu.groceriesstore.di.IoDispatcher +import com.hieuwu.groceriesstore.domain.usecases.RemoveMealFromPlanUseCase +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class RemoveMealFromPlanUseCaseImpl @Inject constructor( + private val mealPlanRepository: MealPlanRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) : RemoveMealFromPlanUseCase { + override suspend fun execute(input: RemoveMealFromPlanUseCase.Input): RemoveMealFromPlanUseCase.Output { + return withContext(ioDispatcher) { + try { + mealPlanRepository.removeMealFromPlan(input.id) + RemoveMealFromPlanUseCase.Output.Success + } catch (e: Exception) { + RemoveMealFromPlanUseCase.Output.Failure + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RetrieveMealByTypeUseCaseImpl.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RetrieveMealByTypeUseCaseImpl.kt new file mode 100644 index 00000000..d234d98b --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/impl/RetrieveMealByTypeUseCaseImpl.kt @@ -0,0 +1,32 @@ +package com.hieuwu.groceriesstore.domain.usecases.impl + +import com.hieuwu.groceriesstore.data.repository.MealPlanRepository +import com.hieuwu.groceriesstore.di.IoDispatcher +import com.hieuwu.groceriesstore.domain.models.MealModel +import com.hieuwu.groceriesstore.domain.usecases.RetrieveMealByTypeUseCase +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class RetrieveMealByTypeUseCaseImpl @Inject constructor( + private val mealPlanRepository: MealPlanRepository, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) : RetrieveMealByTypeUseCase { + override suspend fun execute(input: RetrieveMealByTypeUseCase.Input): RetrieveMealByTypeUseCase.Output { + return withContext(ioDispatcher) { + try { + val result = mealPlanRepository.retrieveMealByType( + type = input.mealType.value, + weekDayValue = input.dayValue.dayValue + ) + RetrieveMealByTypeUseCase.Output.Success(data = result) + } catch (e: Exception) { + RetrieveMealByTypeUseCase.Output.Failure + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountFragment.kt index fbf695e6..360c5832 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountFragment.kt @@ -28,6 +28,7 @@ class AccountFragment : Fragment() { onProfileSettingsClick = ::navigateToProfileSettings, onNotificationSettingsClick = ::navigateToNotificationsSettings, onOrderHistorySettingsClick = ::navigateToOrderHistory, + onMealPlanningClick = ::navigateToMealPlanning, ) } } @@ -49,4 +50,8 @@ class AccountFragment : Fragment() { private fun navigateToOrderHistory() { findNavController().navigate(R.id.action_accountFragment_to_orderHistoryFragment) } + + private fun navigateToMealPlanning() { + findNavController().navigate(R.id.action_accountFragment_to_overviewFragment) + } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountScreen.kt index c4198b18..c92d2006 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountScreen.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/AccountScreen.kt @@ -23,7 +23,9 @@ fun AccountScreen( onProfileSettingsClick: (() -> Unit), onNotificationSettingsClick: (() -> Unit), onOrderHistorySettingsClick: (() -> Unit), -) { + onMealPlanningClick: (() -> Unit), + + ) { val user = viewModel.user.collectAsState() AccountScreenView( @@ -34,6 +36,7 @@ fun AccountScreen( onNotificationSettingsClick = onNotificationSettingsClick, onOrderHistorySettingsClick = onOrderHistorySettingsClick, onSignOutClick = { viewModel.signOut() }, + onMealPlanningClick = onMealPlanningClick ) } @@ -45,6 +48,7 @@ private fun AccountScreenView( onProfileSettingsClick: () -> Unit, onNotificationSettingsClick: () -> Unit, onOrderHistorySettingsClick: () -> Unit, + onMealPlanningClick: () -> Unit, onSignOutClick: () -> Unit, ) { Scaffold { contentPadding -> @@ -62,6 +66,7 @@ private fun AccountScreenView( onNotificationSettingsClick = onNotificationSettingsClick, onOrderHistorySettingsClick = onOrderHistorySettingsClick, onSignOutClick = onSignOutClick, + onMealPlanningClick = onMealPlanningClick, ) } } @@ -86,6 +91,7 @@ private fun SignedInAccountScreen() { onNotificationSettingsClick = { /*TODO*/ }, onOrderHistorySettingsClick = { /*TODO*/ }, onSignOutClick = { /*TODO*/ }, + onMealPlanningClick = {} ) } @@ -98,5 +104,6 @@ private fun SignedOutAccountScreen() { onNotificationSettingsClick = { /*TODO*/ }, onOrderHistorySettingsClick = { /*TODO*/ }, onSignOutClick = { /*TODO*/ }, + onMealPlanningClick = {} ) } \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/widgets/AccountContentView.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/widgets/AccountContentView.kt index 7aa534da..7e12510e 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/widgets/AccountContentView.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/account/widgets/AccountContentView.kt @@ -32,6 +32,7 @@ fun AccountContentView( onProfileSettingsClick: () -> Unit, onNotificationSettingsClick: () -> Unit, onOrderHistorySettingsClick: () -> Unit, + onMealPlanningClick: () -> Unit, onSignOutClick: () -> Unit, ) { if (user != null) { @@ -41,6 +42,7 @@ fun AccountContentView( onNotificationSettingsClick = onNotificationSettingsClick, onOrderHistorySettingsClick = onOrderHistorySettingsClick, onSignOutClick = onSignOutClick, + onMealPlanningClick = onMealPlanningClick ) } else { AccountContentSignedOutView( @@ -56,12 +58,14 @@ fun AccountContentSignedInView( onProfileSettingsClick: (() -> Unit), onNotificationSettingsClick: (() -> Unit), onOrderHistorySettingsClick: (() -> Unit), + onMealPlanningClick: (() -> Unit), onSignOutClick: (() -> Unit), ) { val actions = listOf( R.string.profile_information to onProfileSettingsClick, R.string.notification_settings to onNotificationSettingsClick, R.string.order_history_settings to onOrderHistorySettingsClick, + R.string.meal_planning_settings to onMealPlanningClick, ) for (action in actions) { Row( @@ -120,6 +124,7 @@ private fun AccountContentViewSignedInStatePreview() { onProfileSettingsClick = {}, onNotificationSettingsClick = {}, onOrderHistorySettingsClick = {}, + onMealPlanningClick = {} ) {} } } @@ -134,6 +139,7 @@ private fun AccountContentViewSignedOutStatePreview() { onProfileSettingsClick = {}, onNotificationSettingsClick = {}, onOrderHistorySettingsClick = {}, + onMealPlanningClick = {} ) {} } } \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/composables/IconTextInput.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/composables/IconTextInput.kt index 5bffac04..155bf57b 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/composables/IconTextInput.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/composables/IconTextInput.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.rounded.Backspace +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -23,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.hieuwu.groceriesstore.R +@OptIn(ExperimentalMaterial3Api::class) @Composable fun IconTextInput( leadingIcon: ImageVector, @@ -43,8 +46,7 @@ fun IconTextInput( border = BorderStroke( width = 1.dp, color = colorResource(id = R.color.colorPrimary), - - ), + ), shape = RoundedCornerShape(6.dp) ), value = value, @@ -57,6 +59,11 @@ fun IconTextInput( tint = colorResource(id = R.color.primary_button) ) }, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), trailingIcon = { Icon( modifier = modifier.clickable { diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInFragment.kt index 41503c88..51a663a2 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInFragment.kt @@ -25,8 +25,10 @@ class SignInFragment : Fragment() { SignInScreen( modifier = Modifier.fillMaxSize(), onSignUpClick = { - view?.findNavController() - ?.navigate(R.id.action_signInFragment_to_signUpFragment) + findNavController().navigate(R.id.action_signInFragment_to_signUpFragment) + }, + onSignInSuccess = { + requireActivity().finish() } ) } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInScreen.kt index 6e390330..9ce570d1 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInScreen.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/authentication/signin/SignInScreen.kt @@ -46,6 +46,7 @@ import com.hieuwu.groceriesstore.presentation.authentication.composables.IconTex fun SignInScreen( modifier: Modifier = Modifier, onSignUpClick: () -> Unit, + onSignInSuccess: () -> Unit, viewModel: SignInViewModel = hiltViewModel() ) { val snackbarHostState = remember { SnackbarHostState() } @@ -56,10 +57,10 @@ fun SignInScreen( snackbarHostState.showSnackbar("Account not existed!") } } - LaunchedEffect(Unit) { viewModel.isSignUpSuccessful.collect { snackbarHostState.showSnackbar("Sign in successfully!") + onSignInSuccess() } } @@ -136,7 +137,8 @@ fun SignInScreen( onClick = onSignUpClick, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = colorResource(id = R.color.colorPrimary)), + contentColor = colorResource(id = R.color.colorPrimary) + ), ) { Text("Sign up") } @@ -149,5 +151,5 @@ fun SignInScreen( @Preview @Composable fun SignInScreenPreview() { - SignInScreen(onSignUpClick = {}) + SignInScreen(onSignUpClick = {}, onSignInSuccess = {}) } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/addmeal/AddMealBottomSheet.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/addmeal/AddMealBottomSheet.kt new file mode 100644 index 00000000..f7f5a07f --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/addmeal/AddMealBottomSheet.kt @@ -0,0 +1,122 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.hieuwu.groceriesstore.presentation.mealplanning.addmeal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cookie +import androidx.compose.material.icons.rounded.AddCircle +import androidx.compose.material.icons.rounded.Backspace +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.presentation.authentication.composables.IconTextInput +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.MealAddingState +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.IngredientChip + +@Composable +fun AddMealBottomSheet( + modifier: Modifier = Modifier, + sheetState: SheetState, + mealAddState: MutableState, + onDismissRequest: () -> Unit, + onAddMealClick: () -> Unit, +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState + ) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .height(350.dp) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + ) { + Text( + text = "Add a meal", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = modifier.height(24.dp)) + IconTextInput( + leadingIcon = Icons.Default.Cookie, + trailingIcon = Icons.Rounded.Backspace, + value = mealAddState.value.name.value, + placeholder = "Name of the meal", + onValueChange = { + mealAddState.value.name.value = it + }, + onTrailingIconClick = { + mealAddState.value.name.value = "" + } + ) + Spacer(modifier = modifier.height(8.dp)) + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(3), + verticalItemSpacing = 4.dp, + horizontalArrangement = Arrangement.spacedBy(4.dp), + content = { + items(mealAddState.value.ingredients.value) { photo -> + IngredientChip(text = photo, onDismiss = { + val newList = mutableListOf().apply { + addAll(mealAddState.value.ingredients.value) + remove(photo) + } + mealAddState.value.ingredients.value = newList + }) + } + }, + ) + val newIngredient = remember { mutableStateOf("") } + IconTextInput( + leadingIcon = Icons.Default.Cookie, + trailingIcon = Icons.Rounded.AddCircle, + value = newIngredient.value, + placeholder = "Ingredients", + onValueChange = { + newIngredient.value = it + }, + onTrailingIconClick = { + val newList = mutableListOf().apply { + addAll(mealAddState.value.ingredients.value) + add(newIngredient.value) + } + mealAddState.value.ingredients.value = newList + newIngredient.value = "" + } + ) + Spacer(modifier = modifier.height(8.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onAddMealClick, + colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.colorPrimary)), + ) { + Text("Add this meal") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewFragment.kt new file mode 100644 index 00000000..e9c7b4d9 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewFragment.kt @@ -0,0 +1,28 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class OverviewFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setContent { + OverViewScreen( + onNavigateUpClick = { findNavController().navigateUp() } + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewScreen.kt new file mode 100644 index 00000000..e7a16559 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewScreen.kt @@ -0,0 +1,226 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview + +import androidx.compose.foundation.layout.Arrangement +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NavigateBefore +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.presentation.mealplanning.addmeal.AddMealBottomSheet +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.EmptyListIndicatorText +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.LineTextButton +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.MealItem +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.SwipeToDeleteContainer +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable.WeekDayItem +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.MealType +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OverViewScreen( + modifier: Modifier = Modifier, + viewModel: OverviewViewModel = hiltViewModel(), + onNavigateUpClick: () -> Unit +) { + val breakfastList = viewModel.breakfastMeals.collectAsState().value + val weekDays = viewModel.day.collectAsState().value + val lunchMeals = viewModel.lunchMeals.collectAsState().value + val dinnerMeals = viewModel.dinnerMeals.collectAsState().value + val showBottomSheet = remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + val mealType = remember { mutableStateOf(MealType.BREAKFAST) } + Scaffold(topBar = { + TopAppBar( + title = { + Text( + text = "Today's meals", + color = Color.White + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorResource(id = R.color.colorPrimary) + ), + navigationIcon = { + IconButton(onClick = onNavigateUpClick) { + Icon( + imageVector = Icons.Filled.NavigateBefore, + contentDescription = null, + tint = Color.White + ) + } + } + ) + } + ) { contentPadding -> + val scope = rememberCoroutineScope() + val mealAddState = remember { mutableStateOf(MealAddingState()) } + if (showBottomSheet.value) { + AddMealBottomSheet( + onDismissRequest = { + showBottomSheet.value = false + }, + sheetState = sheetState, + mealAddState = mealAddState, + onAddMealClick = { + viewModel.onAddMeal( + mealType = mealType.value, + name = mealAddState.value.name.value, + ingredients = mealAddState.value.ingredients.value + ) + mealAddState.value.name.value = "" + mealAddState.value.ingredients.value = listOf() + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + showBottomSheet.value = false + } + } + } + ) + } + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(contentPadding) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + LazyVerticalGrid( + modifier = modifier + .fillMaxWidth() + .height(64.dp), + columns = GridCells.Fixed(7) + ) { + itemsIndexed(weekDays) { index, currentDay -> + WeekDayItem( + modifier = modifier, + currentDay = currentDay, + onItemClick = { viewModel.onWeekDaySelected(index) }, + index = index + ) + } + } + } + item { + LineTextButton( + modifier = modifier, + textTitle = "Breakfast", + onButtonClick = { + mealType.value = MealType.BREAKFAST + showBottomSheet.value = true + } + ) + } + if (breakfastList.isNotEmpty()) { + items(breakfastList, key = { it.id }) { meal -> + SwipeToDeleteContainer( + item = meal, + onDelete = { + viewModel.onRemoveMeal(meal.id) + } + ) { meal -> + MealItem(meal = meal, modifier = modifier.padding(16.dp)) + } + } + } else { + item { + EmptyListIndicatorText() + } + } + + item { + LineTextButton( + modifier = modifier, + textTitle = "Lunch", + onButtonClick = { + mealType.value = MealType.LUNCH + showBottomSheet.value = true + } + ) + } + + if (lunchMeals.isNotEmpty()) { + items(lunchMeals, key = { it.id }) { meal -> + SwipeToDeleteContainer( + item = meal, + onDelete = { + viewModel.onRemoveMeal(meal.id) + } + ) { meal -> + MealItem(meal = meal, modifier = modifier.padding(16.dp)) + } + } + } else { + item { + EmptyListIndicatorText() + } + } + + item { + LineTextButton( + modifier = modifier, + textTitle = "Dinner", + onButtonClick = { + mealType.value = MealType.DINNER + showBottomSheet.value = true + } + ) + } + + if (dinnerMeals.isNotEmpty()) { + items(dinnerMeals, key = { it.id }) { meal -> + SwipeToDeleteContainer( + item = meal, + onDelete = { + viewModel.onRemoveMeal(meal.id) + } + ) { meal -> + MealItem(meal = meal, modifier = modifier.padding(16.dp)) + } + } + } else { + item { + EmptyListIndicatorText() + } + } + } + } +} + +data class MealAddingState( + val name: MutableState = mutableStateOf(""), + val ingredients: MutableState> = mutableStateOf(listOf()) +) + +@Composable +@Preview +fun OverViewScreenPreview(modifier: Modifier = Modifier) { + OverViewScreen(modifier, onNavigateUpClick = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewViewModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewViewModel.kt new file mode 100644 index 00000000..feb7c27e --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/OverviewViewModel.kt @@ -0,0 +1,204 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hieuwu.groceriesstore.domain.models.MealModel +import com.hieuwu.groceriesstore.domain.usecases.AddMealToPlanUseCase +import com.hieuwu.groceriesstore.domain.usecases.RemoveMealFromPlanUseCase +import com.hieuwu.groceriesstore.domain.usecases.RetrieveMealByTypeUseCase +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.Meal +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.MealType +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.WeekDay +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.WeekDayValue +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OverviewViewModel @Inject constructor( + private val addMealToPlanUseCase: AddMealToPlanUseCase, + private val retrieveMealByTypeUseCase: RetrieveMealByTypeUseCase, + private val removeMealFromPlanUseCase: RemoveMealFromPlanUseCase +) : ViewModel() { + + private val _days = MutableStateFlow( + WeekDayValue.entries.map { + WeekDay(it.dayValue) + }.toMutableList() + ) + val day: StateFlow> = _days + + private val _breakfastMeals = MutableStateFlow>(value = emptyList()) + val breakfastMeals: StateFlow> = _breakfastMeals + + private val _lunchMeals = MutableStateFlow>(value = emptyList()) + val lunchMeals: StateFlow> = _lunchMeals + + private val _dinnerMeals = MutableStateFlow>(value = emptyList()) + val dinnerMeals: StateFlow> = _dinnerMeals + + private val initialSelectedDay: Int = 0 + private var selectedDayIndex = initialSelectedDay + + init { + retrieveMeal(WeekDayValue.valueOf(_days.value[selectedDayIndex].name)) + _days.value[initialSelectedDay].isSelected.value = true + } + + private fun retrieveMeal(currentDayValue: WeekDayValue) { + viewModelScope.launch { + launch { + onRetrieveMealByType( + weekDayValue = currentDayValue, + mealType = MealType.BREAKFAST, + ) + } + + launch { + onRetrieveMealByType( + weekDayValue = currentDayValue, + mealType = MealType.LUNCH, + ) + } + + launch { + onRetrieveMealByType( + weekDayValue = currentDayValue, + mealType = MealType.DINNER, + ) + } + } + } + + private suspend fun onRetrieveMealByType( + weekDayValue: WeekDayValue, + mealType: MealType, + ) { + val result = retrieveMealByTypeUseCase.execute( + RetrieveMealByTypeUseCase.Input( + dayValue = weekDayValue, + mealType = mealType + ) + ) + when (result) { + is RetrieveMealByTypeUseCase.Output.Success -> { + mapMealByType(mealType = mealType, meals = result.data) + } + + is RetrieveMealByTypeUseCase.Output.Failure -> { + _breakfastMeals.value = listOf() + } + } + } + + private fun MealModel.asDomain(): Meal = Meal( + id = id, + name = name, + imageUrl = "", + ingredients = ingredients.toList() + ) + + private fun mapMealByType(mealType: MealType, meals: List) { + when (mealType) { + MealType.BREAKFAST -> { + _breakfastMeals.value = meals.map { meal -> meal.asDomain() } + } + + MealType.LUNCH -> { + _lunchMeals.value = meals.map { meal -> meal.asDomain() } + } + + MealType.DINNER -> { + _dinnerMeals.value = meals.map { meal -> meal.asDomain() } + } + } + } + + fun onWeekDaySelected(index: Int) { + selectedDayIndex = index + for (i in 0 until _days.value.size) { + _days.value[i].isSelected.value = i == index + } + retrieveMeal(WeekDayValue.valueOf(_days.value[selectedDayIndex].name)) + } + + fun onRemoveMeal(id: String) { + viewModelScope.launch { + removeMealFromPlanUseCase.execute( + RemoveMealFromPlanUseCase.Input( + id = id + ) + ) + } + } + + fun onAddMeal(mealType: MealType, name: String, ingredients: List) { + when (mealType) { + MealType.BREAKFAST -> onAddBreakfast(name, ingredients) + MealType.DINNER -> onAddDinner(name, ingredients) + MealType.LUNCH -> onAddLunch(name, ingredients) + } + } + + private fun onAddBreakfast(name: String, ingredients: List) { + viewModelScope.launch { + addMealToPlanUseCase.execute( + AddMealToPlanUseCase.Input( + name = name, + weekDay = _days.value[selectedDayIndex].name, + ingredients = ingredients, + mealType = MealType.BREAKFAST + ) + ) + _breakfastMeals.value = addMealToList( + mealList = _breakfastMeals.value, + newMeal = Meal(name = name, imageUrl = "", ingredients = ingredients, id = "") + ) + } + } + + private fun onAddLunch(name: String, ingredients: List) { + viewModelScope.launch { + addMealToPlanUseCase.execute( + AddMealToPlanUseCase.Input( + name = name, + weekDay = _days.value[selectedDayIndex].name, + ingredients = ingredients, + mealType = MealType.LUNCH + ) + ) + _lunchMeals.value = addMealToList( + mealList = _lunchMeals.value, + newMeal = Meal(name = name, imageUrl = "", ingredients = ingredients, id = "") + ) + } + } + + private fun onAddDinner(name: String, ingredients: List) { + viewModelScope.launch { + addMealToPlanUseCase.execute( + AddMealToPlanUseCase.Input( + name = name, + weekDay = _days.value[selectedDayIndex].name, + ingredients = ingredients, + mealType = MealType.DINNER + ) + ) + _dinnerMeals.value = addMealToList( + mealList = _dinnerMeals.value, + newMeal = Meal(name = name, imageUrl = "", ingredients = ingredients, id = "") + ) + } + } + + private fun addMealToList(mealList: List, newMeal: Meal): List { + val newList = mutableListOf().apply { + addAll(mealList) + add(newMeal) + } + return newList + } +} + diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/EmptyListIndicatorText.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/EmptyListIndicatorText.kt new file mode 100644 index 00000000..9624b74d --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/EmptyListIndicatorText.kt @@ -0,0 +1,22 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun EmptyListIndicatorText(modifier: Modifier = Modifier) { + Text( + text = "There is no meal. Add a meal to your plan", + style = MaterialTheme.typography.bodyLarge, + modifier = modifier + .fillMaxWidth() + .padding(12.dp), + textAlign = TextAlign.Center + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/IngredientChip.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/IngredientChip.kt new file mode 100644 index 00000000..a8e4211a --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/IngredientChip.kt @@ -0,0 +1,60 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Dining +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +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.res.colorResource +import androidx.compose.ui.unit.dp +import com.hieuwu.groceriesstore.R + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun IngredientChip( + text: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + var enabled by remember { mutableStateOf(true) } + if (!enabled) return + + InputChip( + colors = InputChipDefaults.inputChipColors( + labelColor = colorResource(id = R.color.colorPrimary).copy( + alpha = 0.2f + ) + ), + onClick = { + onDismiss() +// enabled = !enabled + }, + label = { Text(text) }, + selected = enabled, + avatar = { + Icon( + Icons.Filled.Dining, + contentDescription = "Localized description", + Modifier.size(12.dp) + ) + }, + trailingIcon = { + Icon( + Icons.Default.Close, + contentDescription = "Localized description", + Modifier.size(12.dp) + ) + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/LineTextButton.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/LineTextButton.kt new file mode 100644 index 00000000..15a31108 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/LineTextButton.kt @@ -0,0 +1,47 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import com.hieuwu.groceriesstore.R + +@Composable +fun LineTextButton( + modifier: Modifier = Modifier, + textTitle: String, + onButtonClick: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = textTitle, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + TextButton(onClick = onButtonClick) { + Icon( + imageVector = Icons.Default.Add, contentDescription = null, + tint = colorResource(id = R.color.primary_button) + ) + Text( + text = "Add", + color = colorResource(id = R.color.primary_button) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/MealItem.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/MealItem.kt new file mode 100644 index 00000000..634b1aa6 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/MealItem.kt @@ -0,0 +1,83 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.hieuwu.groceriesstore.presentation.core.widgets.WebImage +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.Meal + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MealItem( + modifier: Modifier = Modifier, + meal: Meal +) { + Card( + modifier = modifier + .height(164.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.elevatedCardElevation(), + shape = MaterialTheme.shapes.medium, + ) { + LazyRow( + modifier = modifier + .fillMaxSize() + .fillMaxHeight() + ) { + item { + Row() { + WebImage( + model = meal.imageUrl, + contentDescription = "", + contentScale = ContentScale.FillBounds, + modifier = modifier.size(124.dp) + ) + } + } + item { + LazyColumn( + contentPadding = PaddingValues(horizontal = 4.dp), + modifier = modifier + .fillMaxWidth() + ) { + stickyHeader { + Text( + modifier = modifier + .fillMaxWidth() + .background(Color.White) + .padding(12.dp), + text = meal.name + ) + } + items(meal.ingredients) { item -> + IngredientChip( + text = item, + onDismiss = {}, + modifier = modifier.padding(8.dp) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/SwipeToDeleteContainer.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/SwipeToDeleteContainer.kt new file mode 100644 index 00000000..7cc0eea0 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/SwipeToDeleteContainer.kt @@ -0,0 +1,102 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwipeToDeleteContainer( + item: T, + onDelete: (T) -> Unit, + animationDuration: Int = 500, + content: @Composable (T) -> Unit +) { + var isRemoved by remember { + mutableStateOf(false) + } + val state = rememberDismissState( + confirmValueChange = { value -> + if (value == DismissValue.DismissedToStart) { + isRemoved = true + true + } else { + false + } + } + ) + + LaunchedEffect(key1 = isRemoved) { + if(isRemoved) { + delay(animationDuration.toLong()) + onDelete(item) + } + } + + AnimatedVisibility( + visible = !isRemoved, + exit = shrinkVertically( + animationSpec = tween(durationMillis = animationDuration), + shrinkTowards = Alignment.Top + ) + fadeOut() + ) { + SwipeToDismiss( + state = state, + background = { + DeleteBackground(swipeDismissState = state) + }, + dismissContent = { content(item) }, + directions = setOf(DismissDirection.EndToStart) + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeleteBackground( + swipeDismissState: DismissState +) { + val color = if (swipeDismissState.dismissDirection == DismissDirection.EndToStart) { + Color.Red + } else Color.Transparent + + Box( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(16.dp), + contentAlignment = Alignment.CenterEnd + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = Color.White + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/WeekDayItem.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/WeekDayItem.kt new file mode 100644 index 00000000..7beb50c5 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/composable/WeekDayItem.kt @@ -0,0 +1,57 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.presentation.mealplanning.overview.state.WeekDay + +@Composable +fun WeekDayItem( + modifier: Modifier = Modifier, + currentDay: WeekDay, + onItemClick: (Int) -> Unit, + index: Int, +) { + Box( + modifier = modifier + .height(64.dp) + .padding(8.dp) + .clip( + shape = RoundedCornerShape(6.dp), + ) + .background( + color = if (currentDay.isSelected.value) + colorResource(id = R.color.colorPrimary) + else Color.Transparent + ) + .clickable { + onItemClick(index) + }, + contentAlignment = Alignment.Center + ) { + Text( + modifier = modifier + .fillMaxSize() + .padding(4.dp), + text = currentDay.name, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelLarge, + color = Color.Black + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/Meal.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/Meal.kt new file mode 100644 index 00000000..c0248b8b --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/Meal.kt @@ -0,0 +1,8 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.state + +data class Meal( + val id: String, + val name: String, + val imageUrl: String, + val ingredients: List = listOf(), +) \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/MealType.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/MealType.kt new file mode 100644 index 00000000..075581d4 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/MealType.kt @@ -0,0 +1,5 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.state + +enum class MealType(val value: String) { + LUNCH("lunch"), BREAKFAST("breakfast"), DINNER("dinner") +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDay.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDay.kt new file mode 100644 index 00000000..9e3dbbbe --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDay.kt @@ -0,0 +1,9 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.state + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf + +data class WeekDay( + val name: String, + val isSelected: MutableState = mutableStateOf(false), +) \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDayValue.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDayValue.kt new file mode 100644 index 00000000..3cdc991f --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/mealplanning/overview/state/WeekDayValue.kt @@ -0,0 +1,11 @@ +package com.hieuwu.groceriesstore.presentation.mealplanning.overview.state + +enum class WeekDayValue(val dayValue: String) { + Mon("Mon"), + Tue("Tue"), + Wed("Wed"), + Thu("Thu"), + Fri("Fri"), + Sat("Sat"), + Sun("Sun"), +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/orderhistory/OrderHistoryFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/orderhistory/OrderHistoryFragment.kt index 791c7c04..1e688ccb 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/orderhistory/OrderHistoryFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/orderhistory/OrderHistoryFragment.kt @@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class OrderHistoryFragment : Fragment() { - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml index 55968dcc..2c84e801 100644 --- a/app/src/main/res/navigation/navigation.xml +++ b/app/src/main/res/navigation/navigation.xml @@ -12,6 +12,10 @@ android:id="@+id/action_accountFragment_to_updateProfileFragment" app:destination="@id/updateProfileFragment" /> + + @@ -24,6 +28,12 @@ app:enterAnim="@anim/nav_default_enter_anim" app:exitAnim="@anim/nav_default_exit_anim" /> + + + + tools:layout="@layout/fragment_explore"> @@ -104,7 +114,7 @@ + android:label="DeliveryFragment"> @@ -112,7 +122,7 @@ + android:label="ProductListFragment"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16b17b02..1d3a0ff8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,8 @@ Update profile failed! Notification settings Order history + Meal Planning + Notifications Show when order created