diff --git a/app/build.gradle b/app/build.gradle index 00354b81..1530dd51 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,10 +113,14 @@ dependencies { implementation "androidx.compose.ui:ui-tooling" implementation "com.google.accompanist:accompanist-themeadapter-material:0.28.0" implementation("androidx.hilt:hilt-navigation-compose:1.0.0") + implementation "com.github.bumptech.glide:compose:1.0.0-alpha.1" + implementation "androidx.compose.material3:material3:1.1.1" - + debugImplementation "androidx.compose.ui:ui-tooling:1.4.2" + implementation "androidx.compose.ui:ui-tooling-preview:1.4.2" def nav_version = "2.5.3" implementation("androidx.navigation:navigation-compose:$nav_version") + implementation "androidx.compose.material:material-icons-extended:1.0.0" implementation libs.caruilib diff --git a/app/src/androidTest/java/com/hieuwu/groceriesstore/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/hieuwu/groceriesstore/ExampleInstrumentedTest.kt deleted file mode 100644 index ca1f8a16..00000000 --- a/app/src/androidTest/java/com/hieuwu/groceriesstore/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -// package com.hieuwu.groceriesstore -// -// import androidx.test.InstrumentationRegistry -// import androidx.test.runner.AndroidJUnit4 -// import org.junit.Test -// import org.junit.runner.RunWith -// -// import org.junit.Assert.* -// -// /** -// * Instrumented test, which will execute on an Android device. -// * -// * See [testing documentation](http://d.android.com/tools/testing). -// */ -// @RunWith(AndroidJUnit4::class) -// class ExampleInstrumentedTest { -// @Test -// fun useAppContext() { -// // Context of the app under test. -// val appContext = InstrumentationRegistry.getInstrumentation().targetContext -// assertEquals("com.hieuwu.groceriesstore", appContext.packageName) -// } -// } diff --git a/app/src/androidTest/java/com/hieuwu/groceriesstore/GroceriesStoreDatabaseTest.kt b/app/src/androidTest/java/com/hieuwu/groceriesstore/GroceriesStoreDatabaseTest.kt deleted file mode 100644 index 561df9f9..00000000 --- a/app/src/androidTest/java/com/hieuwu/groceriesstore/GroceriesStoreDatabaseTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.hieuwu.groceriesstore - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.asLiveData -import androidx.room.Room -import androidx.test.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 -import com.hieuwu.groceriesstore.data.database.GroceriesStoreDatabase -import com.hieuwu.groceriesstore.data.database.dao.LineItemDao -import com.hieuwu.groceriesstore.data.database.dao.OrderDao -import com.hieuwu.groceriesstore.data.database.dao.ProductDao -import com.hieuwu.groceriesstore.data.database.entities.LineItem -import com.hieuwu.groceriesstore.data.database.entities.Order -import com.hieuwu.groceriesstore.data.database.entities.Product -import com.hieuwu.groceriesstore.utilities.OrderStatus -import java.io.IOException -import junit.framework.Assert.assertEquals -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class GroceriesStoreDatabaseTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - - private lateinit var productDao: ProductDao - private lateinit var linetItemDao: LineItemDao - private lateinit var orderDao: OrderDao - - private lateinit var db: GroceriesStoreDatabase - - @Before - fun createDb() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - // Using an in-memory database because the information stored here disappears when the - // process is killed. - db = Room.inMemoryDatabaseBuilder(context, GroceriesStoreDatabase::class.java) - // Allowing main thread queries, just for testing. - .allowMainThreadQueries() - .build() - productDao = db.productDao() - linetItemDao = db.lineItemDao() - orderDao = db.orderDao() - } - - @After - @Throws(IOException::class) - fun closeDb() { - db.close() - } - - @Test - @Throws(Exception::class) - suspend fun insertAndGetProduct() { - val aProduct = Product("123", "Product Test", "Test", 12.0, "", "", "") - withContext(Dispatchers.IO) { - productDao.insert(aProduct) - } - var product = productDao.getById(aProduct.id).asLiveData() - assertEquals(product.getOrAwaitValue().price, 12.0) - } - - @Test - @Throws(Exception::class) - suspend fun insertAndGetLineItem() { - val aProduct = Product("123", "Product Test", "Test", 12.0, "", "", "") - withContext(Dispatchers.IO) { - productDao.insert(aProduct) - } - val lineItem = LineItem(1, aProduct.id, "123", 4, 5.6) - linetItemDao.insert(lineItem) - var lineItemAfter = linetItemDao.getById(lineItem.id).asLiveData() - - assertEquals(lineItemAfter.getOrAwaitValue().productId, "123") - assertEquals(lineItemAfter.getOrAwaitValue().quantity, 4) - assertEquals(lineItemAfter.getOrAwaitValue().subtotal, 5.6) - } - - @Test - @Throws(Exception::class) - suspend fun insertAndGetOrderWithItems() { - val firstProduct = Product("1", "First Product", "Test", 12.0, "empty", "", "") - val secondProduct = Product("2", "Second Product", "Test", 13.0, "empty", "", "") - withContext(Dispatchers.IO) { - productDao.insert(firstProduct) - productDao.insert(secondProduct) - } - - val order = Order("12", OrderStatus.IN_CART.value, "") - - val firstLineItem = LineItem(1, firstProduct.id, "12", 4, 5.6) - val secondLineItem = LineItem(2, secondProduct.id, "12", 5, 7.0) - - orderDao.insert(order) - linetItemDao.insert(firstLineItem) - linetItemDao.insert(secondLineItem) - - var completedOrder = orderDao.getById("12") - var a = completedOrder - assertEquals(completedOrder, null) - } - - @Test - @Throws(Exception::class) - fun insertAndGetCurrentCart() { - val order = Order("12", OrderStatus.IN_CART.value, "") - orderDao.insert(order) - - var cart = orderDao.getCart(OrderStatus.IN_CART.value).asLiveData() - var a = cart.getOrAwaitValue() - assertEquals(true, true) - } - - @Test - @Throws(Exception::class) - suspend fun removeItem() { - val firstProduct = Product("1", "First Product", "Test", 12.0, "empty", "", "") - val secondProduct = Product("2", "Second Product", "Test", 13.0, "empty", "", "") - - withContext(Dispatchers.IO) { - productDao.insert(firstProduct) - productDao.insert(secondProduct) - } - - val order = Order("12", OrderStatus.IN_CART.value, "") - - val firstLineItem = LineItem(1, firstProduct.id, "12", 4, 5.6) - val secondLineItem = LineItem(2, secondProduct.id, "12", 5, 7.0) - - orderDao.insert(order) - linetItemDao.insert(firstLineItem) - linetItemDao.insert(secondLineItem) - var cart = orderDao.getCart(OrderStatus.IN_CART.value).asLiveData() - - withContext(Dispatchers.IO) { - linetItemDao.remove(firstLineItem) - } - - var list = linetItemDao.getAll().asLiveData() - var data = list.getOrAwaitValue() - assertEquals(null, null) - } -} diff --git a/app/src/androidTest/java/com/hieuwu/groceriesstore/LiveDataTestUtil.kt b/app/src/androidTest/java/com/hieuwu/groceriesstore/LiveDataTestUtil.kt deleted file mode 100644 index c1e48d19..00000000 --- a/app/src/androidTest/java/com/hieuwu/groceriesstore/LiveDataTestUtil.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.hieuwu.groceriesstore - -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -/** - * Gets the value of a [LiveData] or waits for it to have one, with a timeout. - * - * Use this extension from host-side (JVM) tests. It's recommended to use it alongside - * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously. - */ -fun LiveData.getOrAwaitValue( - time: Long = 2, - timeUnit: TimeUnit = TimeUnit.SECONDS, - afterObserve: () -> Unit = {} -): T { - var data: T? = null - val latch = CountDownLatch(1) - val observer = object : Observer { - override fun onChanged(o: T?) { - data = o - latch.countDown() - this@getOrAwaitValue.removeObserver(this) - } - } - this.observeForever(observer) - - afterObserve.invoke() - - // Don't wait indefinitely if the LiveData is not set. - if (!latch.await(time, timeUnit)) { - this.removeObserver(observer) - throw TimeoutException("LiveData value was never set.") - } - - @Suppress("UNCHECKED_CAST") - return data as T -} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/di/SharePrefModule.kt b/app/src/main/java/com/hieuwu/groceriesstore/di/SharePrefModule.kt new file mode 100644 index 00000000..24af9c86 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/di/SharePrefModule.kt @@ -0,0 +1,25 @@ +package com.hieuwu.groceriesstore.di + +import android.content.Context +import android.content.SharedPreferences +import com.hieuwu.groceriesstore.R +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object SharePrefModule { + + @Provides + @Singleton + fun provideSharePrefs(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences( + context.getString(R.string.sync_status_pref_name), + Context.MODE_PRIVATE + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/SwipeGestures.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/SwipeGestures.kt deleted file mode 100644 index 46536603..00000000 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/SwipeGestures.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.hieuwu.groceriesstore.presentation.adapters - -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import com.hieuwu.groceriesstore.R - -class SwipeToDeleteCallback constructor(val adapter: LineListItemAdapter) : - ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { - - private var _icon: Drawable? = - ContextCompat.getDrawable(adapter.context, R.drawable.ic_baseline_delete) - private var _background: ColorDrawable = ColorDrawable(Color.RED) - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - return false - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val position = viewHolder.adapterPosition - adapter.removeItemAt(position) - } - - override fun onChildDraw( - c: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - dY: Float, - actionState: Int, - isCurrentlyActive: Boolean - ) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) - val itemView = viewHolder.itemView - val backgroundCornerOffset = 20 - val iconMargin = (itemView.height - _icon?.intrinsicHeight!!) / 2 - val iconTop = itemView.top + (itemView.height - _icon?.intrinsicHeight!!) / 2 - val iconBottom = iconTop + _icon?.intrinsicHeight!! - - when { - // Swiping to the left - dX < 0 -> { - val iconLeft = itemView.right - iconMargin - _icon?.intrinsicWidth!! - val iconRight = itemView.right - iconMargin - _icon?.setBounds(iconLeft, iconTop, iconRight, iconBottom) - - _background.setBounds( - itemView.right + dX.toInt() - backgroundCornerOffset, - itemView.top, itemView.right, itemView.bottom - ) - } - else -> { - _background.setBounds(0, 0, 0, 0) - } - } - _background.draw(c) - _icon?.draw(c) - } -} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/ViewPagerAdapter.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/ViewPagerAdapter.kt deleted file mode 100644 index 911f0f2a..00000000 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/adapters/ViewPagerAdapter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.hieuwu.groceriesstore.presentation.adapters - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.viewpager.widget.PagerAdapter -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.presentation.utils.bindImage - -class ViewPagerAdapter(val context: Context) : PagerAdapter() { - - private val images = arrayOf( - "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664736648315.png?alt=media&token=2ca8be3a-37c3-4c73-b966-dcf3129958fd", - "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664737376188.png?alt=media&token=01123878-812e-4b2b-be91-d9c05b1e9b98", - "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664737421330.png?alt=media&token=66f45505-ce68-4583-b018-2d2855aa714e" - ) - - override fun instantiateItem(container: ViewGroup, position: Int): Any { - val layoutInflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val view = layoutInflater.inflate(R.layout.layout_image_layout, null) - val imageView = view.findViewById(R.id.imageView) - bindImage(imageView, images[position]) - container.addView(view, 0) - - return view - } - - override fun getCount() = images.size - - override fun isViewFromObject(view: View, `object`: Any) = view == `object` - - override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { - container.removeView(`object` as View) - } -} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartFragment.kt index 31a36351..9b62da92 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartFragment.kt @@ -1,85 +1,30 @@ package com.hieuwu.groceriesstore.presentation.cart import android.os.Bundle -import android.view.ContextMenu import android.view.LayoutInflater -import android.view.MenuInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.viewModels -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ItemTouchHelper +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.databinding.FragmentCartBinding -import com.hieuwu.groceriesstore.presentation.adapters.LineListItemAdapter -import com.hieuwu.groceriesstore.presentation.adapters.SwipeToDeleteCallback -import com.hieuwu.groceriesstore.presentation.shop.ShopFragmentDirections import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint class CartFragment : BottomSheetDialogFragment() { - private lateinit var binding: FragmentCartBinding - private val viewModel: CartViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_cart, container, false - ) - binding.viewModel = viewModel - binding.lifecycleOwner = this - - setEventListener() - setObserver() - registerForContextMenu(binding.cartDetailRecyclerview) - return binding.root - } - - private fun setEventListener() { - val adapter = LineListItemAdapter( - LineListItemAdapter.OnClickListener( - minusListener = { viewModel.decreaseQty(it) }, - plusListener = { viewModel.increaseQty(it) }, - removeListener = { viewModel.removeItem(it) } - ), requireContext() - ) - - // Setup recyclerview - val itemTouchHelper = - ItemTouchHelper(SwipeToDeleteCallback(adapter)) - itemTouchHelper.attachToRecyclerView(binding.cartDetailRecyclerview) - binding.cartDetailRecyclerview.adapter = adapter - - binding.checkoutButton.setOnClickListener { - val direction = - ShopFragmentDirections.actionShopFragmentToCheckOutFragment( - viewModel.order.value?.id as String + return ComposeView(requireContext()).apply { + setContent { + CartScreen( + modifier = Modifier.fillMaxWidth() ) - this.findNavController().navigate(direction) - dismiss() - } - } - private fun setObserver() { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.order.flowWithLifecycle(lifecycle).collect {} + } } } - override fun onCreateContextMenu( - menu: ContextMenu, - v: View, - menuInfo: ContextMenu.ContextMenuInfo? - ) { - super.onCreateContextMenu(menu, v, menuInfo) - val inflater = MenuInflater(context) - inflater.inflate(R.menu.line_item_context_menu, menu) - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartScreen.kt new file mode 100644 index 00000000..3bed93c9 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/CartScreen.kt @@ -0,0 +1,113 @@ +package com.hieuwu.groceriesstore.presentation.cart + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.rememberDismissState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.presentation.cart.composables.CartItem + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CartScreen( + modifier: Modifier = Modifier, + viewModel: CartViewModel = hiltViewModel() +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + val lineItems = viewModel.order.collectAsState().value?.lineItemList?.toList() ?: listOf() + val total = viewModel.order.collectAsState().value?.total + + Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) { + Text( + text = "My basket", style = MaterialTheme.typography.titleMedium, + color = colorResource(id = R.color.colorPrimary) + ) + Text( + text = "$ $total", + style = MaterialTheme.typography.titleMedium, + color = colorResource(id = R.color.colorPrimary) + ) + + } + LazyColumn(modifier = modifier.fillMaxWidth()) { + items(lineItems) { item -> + val state = rememberDismissState( + confirmStateChange = { + if (it == DismissValue.DismissedToStart) { + viewModel.removeItem(item) + } + true + } + ) + SwipeToDismiss( + state = state, + background = { + val color by animateColorAsState( + targetValue = when (state.dismissDirection) { + DismissDirection.StartToEnd -> colorResource(id = R.color.colorPrimary) + DismissDirection.EndToStart -> colorResource(id = R.color.colorPrimary).copy( + alpha = 0.2f + ) + null -> Color.Transparent + } + ) + Box( + modifier = modifier + .fillMaxSize() + .background(color = color) + .padding(10.dp), + contentAlignment = Alignment.CenterEnd + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = colorResource(id = R.color.colorPrimary), + ) + } + + }, + dismissContent = { + CartItem( + onIncrease = { viewModel.increaseQty(item) }, + onDecrease = { viewModel.decreaseQty(item) }, + onRemove = { viewModel.removeItem(item) }, + lineItem = item + ) + }, + directions = setOf(DismissDirection.EndToStart), + ) + } + } + } + +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/composables/CartItem.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/composables/CartItem.kt new file mode 100644 index 00000000..f9375c76 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/cart/composables/CartItem.kt @@ -0,0 +1,124 @@ +package com.hieuwu.groceriesstore.presentation.cart.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.domain.models.LineItemModel + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun CartItem( + modifier: Modifier = Modifier, + onIncrease: () -> Unit, + onDecrease: () -> Unit, + onRemove: () -> Unit, + lineItem: LineItemModel +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + elevation = 2.dp, + ) { + Row(modifier = modifier.fillMaxWidth()) { + GlideImage( + contentScale = ContentScale.Crop, + model = lineItem.image, + contentDescription = null, + modifier = Modifier + .width(120.dp) + .height(80.dp) + .padding(8.dp) + ) + Column( + modifier = modifier + .weight(1f) + .padding(8.dp) + ) { + Text( + text = lineItem.name ?: "", + style = MaterialTheme.typography.labelLarge + ) + Spacer(modifier = modifier.height(4.dp)) + Text( + text = "$ ${lineItem.price}" + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + IconButton(onClick = onDecrease) { + Icon(imageVector = Icons.Filled.Remove, contentDescription = null) + } + Spacer(modifier = modifier.width(4.dp)) + Text(text = "${lineItem.quantity}") + Spacer(modifier = modifier.width(4.dp)) + IconButton(onClick = onIncrease) { + Icon( + imageVector = Icons.Filled.Add, contentDescription = null, + tint = colorResource(id = R.color.primary_button) + ) + } + } + } + Column(modifier = modifier.padding(8.dp)) { + Text( + text = "$ ${lineItem.subtotal}", + style = MaterialTheme.typography.labelLarge + ) + IconButton( + onClick = onRemove + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = colorResource(id = R.color.colorPrimary) + ) + } + } + } + } + +} + +@Preview +@Composable +fun CartItemPreview() { + CartItem( + onDecrease = {}, + onIncrease = {}, + onRemove = {}, + lineItem = LineItemModel( + id = null, + name = "Fishing", + price = 12.3, + image = "", + quantity = 2, + subtotal = 24.23 + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteFragment.kt index f9b94243..4be88623 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteFragment.kt @@ -4,35 +4,26 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.databinding.FragmentFavouriteBinding -import com.hieuwu.groceriesstore.presentation.adapters.RecipeItemAdapter import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class FavouriteFragment : Fragment() { - private lateinit var binding: FragmentFavouriteBinding - - private val viewModel: FavouriteViewModel by viewModels() - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_favourite, container, false - ) - binding.viewModel = viewModel - binding.lifecycleOwner = this - binding.recipesRecyclerview.adapter = - RecipeItemAdapter(RecipeItemAdapter.OnClickListener()) + return ComposeView(requireContext()).apply { + setContent { + FavouriteScreen(modifier = Modifier.fillMaxWidth()) + } + } - viewModel.recipesList.observe(viewLifecycleOwner) {} - return binding.root } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteScreen.kt new file mode 100644 index 00000000..d4971fcd --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/FavouriteScreen.kt @@ -0,0 +1,23 @@ +package com.hieuwu.groceriesstore.presentation.favourite + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import com.hieuwu.groceriesstore.presentation.favourite.composables.RecipeItem + +@Composable +fun FavouriteScreen( + modifier: Modifier = Modifier, + viewModel: FavouriteViewModel = hiltViewModel() +) { + val recipes = viewModel.recipesList.observeAsState().value + LazyColumn { + items(recipes!!) { item -> + RecipeItem(recipe = item) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/composables/RecipeItem.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/composables/RecipeItem.kt new file mode 100644 index 00000000..3a2daef2 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/favourite/composables/RecipeItem.kt @@ -0,0 +1,47 @@ +package com.hieuwu.groceriesstore.presentation.favourite.composables + +import androidx.compose.foundation.layout.Column +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.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.hieuwu.groceriesstore.domain.models.RecipeModel + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun RecipeItem( + modifier: Modifier = Modifier, + recipe: RecipeModel +) { + Card( + modifier = modifier + .fillMaxWidth() + .height(320.dp) + .padding(8.dp) + ) { + Column( + modifier = modifier.fillMaxSize().padding(16.dp) + ) { + GlideImage( + contentScale = ContentScale.Crop, + model = recipe.image, + contentDescription = null, + modifier = modifier.weight(1f) + ) + Text( + modifier = modifier.padding(vertical = 6.dp), + text = recipe.name, + style = MaterialTheme.typography.labelLarge + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/notificationsettings/NotificationSettingsFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/notificationsettings/NotificationSettingsFragment.kt index 3d95c3b8..f3e6b247 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/notificationsettings/NotificationSettingsFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/notificationsettings/NotificationSettingsFragment.kt @@ -18,7 +18,6 @@ import androidx.navigation.fragment.findNavController import com.hieuwu.groceriesstore.R import com.hieuwu.groceriesstore.databinding.FragmentNotificationSettingsBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingActivity.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingActivity.kt index af3b5561..8483e2f2 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingActivity.kt @@ -1,14 +1,12 @@ package com.hieuwu.groceriesstore.presentation.onboarding -import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil -import androidx.lifecycle.lifecycleScope import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import com.hieuwu.groceriesstore.MainActivity @@ -23,20 +21,10 @@ class OnboardingActivity : AppCompatActivity() { lateinit var binding: ActivityOnboardingBinding private val viewModel: OnboardingViewModel by viewModels() - private lateinit var sharedPreferences: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setTheme(R.style.AppTheme) - - sharedPreferences = - getSharedPreferences(getString(R.string.sync_status_pref_name), Context.MODE_PRIVATE) - - val isSyncedSuccessful = - sharedPreferences.getBoolean(getString(R.string.sync_success), false) - - if (isSyncedSuccessful) navigateToMainInitialScreen() - binding = DataBindingUtil.setContentView(this, R.layout.activity_onboarding) binding.viewModel = viewModel binding.lifecycleOwner = this @@ -58,10 +46,6 @@ class OnboardingActivity : AppCompatActivity() { .flowWithLifecycle(lifecycle) .collect { if (it) { - with(sharedPreferences.edit()) { - putBoolean(getString(R.string.sync_success), true) - apply() - } binding.getStartedButton.isEnabled = true } } @@ -79,8 +63,7 @@ class OnboardingActivity : AppCompatActivity() { }) binding.getStartedButton.setOnClickListener { - val intent = Intent(this, MainActivity::class.java) - startActivity(intent) + navigateToMainInitialScreen() } } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingViewModel.kt index 1e4406bf..d888f966 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/onboarding/OnboardingViewModel.kt @@ -1,5 +1,6 @@ package com.hieuwu.groceriesstore.presentation.onboarding +import android.content.SharedPreferences import androidx.lifecycle.viewModelScope import com.hieuwu.groceriesstore.domain.usecases.RefreshAppDataUseCase import com.hieuwu.groceriesstore.presentation.utils.ObservableViewModel @@ -7,12 +8,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class OnboardingViewModel @Inject constructor( - private val refreshAppDataUseCase: RefreshAppDataUseCase + private val refreshAppDataUseCase: RefreshAppDataUseCase, + private val sharedPreferences: SharedPreferences ) : ObservableViewModel() { private val _isSyncedSuccessful = MutableStateFlow(false) @@ -20,17 +22,22 @@ class OnboardingViewModel @Inject constructor( get() = _isSyncedSuccessful.asStateFlow() init { - try { - viewModelScope.launch { - refreshAppDataUseCase.execute(Unit) + val isSyncedSuccessfully = sharedPreferences.getBoolean("PRODUCT_SYNC_SUCCESS", false) + if (isSyncedSuccessfully) { + _isSyncedSuccessful.value = true + } else { + try { + viewModelScope.launch { + refreshAppDataUseCase.execute(Unit) + with(sharedPreferences.edit()) { + putBoolean("PRODUCT_SYNC_SUCCESS", true) + apply() + _isSyncedSuccessful.value = true + } + } + } catch (e: Exception) { + _isSyncedSuccessful.value = false } - updateSyncStatus(true) - } catch (e: Exception) { - updateSyncStatus(false) } } - - private fun updateSyncStatus(status: Boolean) { - _isSyncedSuccessful.value = status - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailFragment.kt index 51bc35c0..955708e6 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailFragment.kt @@ -1,105 +1,30 @@ package com.hieuwu.groceriesstore.presentation.productdetail import android.os.Bundle -import android.transition.TransitionInflater import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.NestedScrollView -import androidx.databinding.DataBindingUtil +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.databinding.FragmentProductDetailBinding -import com.hieuwu.groceriesstore.utilities.showMessageSnackBar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import timber.log.Timber @AndroidEntryPoint class ProductDetailFragment : Fragment() { - private lateinit var binding: FragmentProductDetailBinding - private val viewModel: ProductDetailViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - - binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_product_detail, container, false - ) - - binding.viewModel = viewModel - binding.lifecycleOwner = this - - setObserver() - - seEventListener() - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - - return binding.root - } - - private fun setObserver() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.product.collect{} - } - - launch { - viewModel.currentCart.collect{} - } - - launch { - viewModel.showSnackBarEvent.collect { - if (it) { // Observed state is true. - showMessageSnackBar(viewModel.qty.toString() + " x " + - viewModel.product.value?.name + " is added") - // Reset state to make sure the snackbar is only shown once, even if the device - // has a configuration change. - viewModel.doneShowingSnackbar() - } + return ComposeView(requireContext()).apply { + setContent { + ProductDetailScreen( + onNavigateBack = { + findNavController().navigateUp() } - } + ) } } } - - private fun seEventListener() { - var isToolbarShown = false - binding.productDetailScrollview.setOnScrollChangeListener( - NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> - Timber.d("$scrollY") - - // User scrolled past image to height of toolbar and the title text is - // underneath the toolbar, so the toolbar should be shown. - val shouldShowToolbar = scrollY > binding.toolbar.height - - // The new state of the toolbar differs from the previous state; update - // appbar and toolbar attributes. - if (isToolbarShown != shouldShowToolbar) { - isToolbarShown = shouldShowToolbar - - // Use shadow animator to add elevation if toolbar is shown - binding.appbar.isActivated = shouldShowToolbar - - // Show the plant name if toolbar is shown - binding.toolbarLayout.isTitleEnabled = shouldShowToolbar - } - }) - - binding.toolbar.setNavigationOnClickListener { - findNavController().navigateUp() - } - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailScreen.kt new file mode 100644 index 00000000..02b408dc --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailScreen.kt @@ -0,0 +1,165 @@ +package com.hieuwu.groceriesstore.presentation.productdetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.NavigateBefore +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.hieuwu.groceriesstore.R +import kotlinx.coroutines.launch + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun ProductDetailScreen( + modifier: Modifier = Modifier, + viewModel: ProductDetailViewModel = hiltViewModel(), + onNavigateBack: () -> Unit +) { + val product = viewModel.product.collectAsState().value + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBar( + title = { + Text("Product details") + }, + backgroundColor = colorResource(id = R.color.colorPrimary), + contentColor = Color.White, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.Filled.NavigateBefore, + contentDescription = null + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .padding(paddingValues) + .padding(horizontal = 20.dp) + ) { + GlideImage( + contentScale = ContentScale.Crop, + model = product?.image, + contentDescription = null, + modifier = modifier + .fillMaxWidth() + .height(230.dp) + ) + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = product?.name ?: "", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "$ ${product?.price}", + style = MaterialTheme.typography.titleMedium + ) + } + Spacer(modifier = modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + val quantity = viewModel.quantity.collectAsState().value + IconButton(onClick = { + viewModel.decreaseQty() + }) { + Icon(imageVector = Icons.Filled.Remove, contentDescription = null) + } + Spacer(modifier = modifier.width(8.dp)) + Text(text = "$quantity") + Spacer(modifier = modifier.width(8.dp)) + IconButton(onClick = { + viewModel.increaseQty() + }) { + Icon( + imageVector = Icons.Filled.Add, contentDescription = null, + tint = colorResource(id = R.color.primary_button) + ) + } + } + Spacer(modifier = modifier.height(8.dp)) + Text( + text = "Description", + style = MaterialTheme.typography.titleMedium + ) + Text(text = product?.description ?: "") + Spacer(modifier = modifier.height(12.dp)) + Text( + text = "Nutrition", + style = MaterialTheme.typography.titleMedium + ) + Text(text = product?.nutrition ?: "") + Spacer(modifier = modifier.height(12.dp)) + Button( + modifier = modifier + .fillMaxWidth(), + onClick = { + viewModel.addToCart() + scope.launch { + snackbarHostState.showSnackbar( + "Added ${product?.name}" + ) + } + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.primary_button), + contentColor = Color.White + ) + ) { + Text(text = "Add to Basket") + } + } + } + +} + +@Preview +@Composable +fun ProductDetailScreenPreview() { + ProductDetailScreen( + onNavigateBack = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailViewModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailViewModel.kt index 1ebb8bf2..8dd21235 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailViewModel.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productdetail/ProductDetailViewModel.kt @@ -1,25 +1,22 @@ package com.hieuwu.groceriesstore.presentation.productdetail -import androidx.databinding.Bindable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.hieuwu.groceriesstore.BR import com.hieuwu.groceriesstore.data.database.entities.LineItem import com.hieuwu.groceriesstore.data.database.entities.Order -import com.hieuwu.groceriesstore.domain.models.OrderModel import com.hieuwu.groceriesstore.data.repository.OrderRepository +import com.hieuwu.groceriesstore.domain.models.OrderModel import com.hieuwu.groceriesstore.domain.usecases.GetProductDetailUseCase import com.hieuwu.groceriesstore.presentation.utils.ObservableViewModel import com.hieuwu.groceriesstore.utilities.OrderStatus import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.UUID +import java.util.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class ProductDetailViewModel @Inject constructor( @@ -40,29 +37,26 @@ class ProductDetailViewModel @Inject constructor( orderRepository.getOneOrderByStatus(OrderStatus.IN_CART) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - private var _qty: Int = 1 - var qty: Int - @Bindable - get() = _qty - set(value) { - _qty = value - notifyPropertyChanged(BR.qty) - } + private val _quantity = MutableStateFlow(1) + val quantity: StateFlow + get() = _quantity - private val _showSnackbarEvent = MutableStateFlow(false) - val showSnackBarEvent: StateFlow - get() = _showSnackbarEvent.asStateFlow() + init { + viewModelScope.launch { + currentCart.collect{} + } + } fun addToCart() { viewModelScope.launch { - val subtotal = product.value?.price?.times(qty) ?: 0.0 + val subtotal = product.value?.price?.times(_quantity.value) ?: 0.0 if (currentCart.value != null) { // Add to cart val cartId = currentCart.value!!.id val lineItem = LineItem( productId = product.value!!.id, orderId = cartId, - quantity = _qty, + quantity = _quantity.value, subtotal = subtotal ) orderRepository.addLineItem(lineItem) @@ -77,25 +71,21 @@ class ProductDetailViewModel @Inject constructor( val lineItem = LineItem( productId = product.value!!.id, orderId = id, - quantity = _qty, + quantity = _quantity.value, subtotal = subtotal ) orderRepository.addLineItem(lineItem) } - _showSnackbarEvent.value = true } } fun increaseQty() { - qty++ + _quantity.value++ } fun decreaseQty() { - if (qty <= 1) return - qty-- + if (_quantity.value <= 1) return + _quantity.value-- } - fun doneShowingSnackbar() { - _showSnackbarEvent.value = false - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopFragment.kt index 45ed392b..05b68303 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopFragment.kt @@ -1,120 +1,27 @@ package com.hieuwu.groceriesstore.presentation.shop -import android.graphics.drawable.Drawable import android.os.Bundle -import android.transition.TransitionInflater import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import androidx.databinding.DataBindingUtil +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.viewpager.widget.ViewPager -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.databinding.FragmentShopBinding -import com.hieuwu.groceriesstore.domain.models.ProductModel -import com.hieuwu.groceriesstore.presentation.adapters.GridListItemAdapter -import com.hieuwu.groceriesstore.presentation.adapters.ViewPagerAdapter -import com.hieuwu.groceriesstore.utilities.showMessageSnackBar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint class ShopFragment : Fragment() { - - private val viewModel: ShopViewModel by viewModels() - private lateinit var dots: Array - private lateinit var binding: FragmentShopBinding - private lateinit var gridListItemAdapter: GridListItemAdapter - private lateinit var nonActiveDot: Drawable - private lateinit var activeDot: Drawable - private var dotCount: Int = 0 - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_shop, container, false - ) - - binding.viewModel = viewModel - binding.lifecycleOwner = this - nonActiveDot = - ContextCompat.getDrawable(requireContext(), R.drawable.non_active_dot_shape)!! - activeDot = ContextCompat.getDrawable(requireContext(), R.drawable.active_dot_shape)!! - setObserver() - setUpRecyclerView() - drawSliderDotSymbols() - setEventListener() - - val inflater = TransitionInflater.from(requireContext()) - exitTransition = inflater.inflateTransition(R.transition.fade) - - - return binding.root - } - - private fun drawSliderDotSymbols() { - val viewPagerAdapter = ViewPagerAdapter(requireContext()) - binding.viewPager.adapter = viewPagerAdapter - dotCount = viewPagerAdapter.count - val sliderDotspanel = binding.sliderDots - dots = Array(dotCount) { ImageView(requireContext()) } - val params = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - params.setMargins(10, 0, 10, 0) - repeat(dotCount) { - dots[it].setImageDrawable(nonActiveDot) - sliderDotspanel.addView(dots[it], params) - } - dots[0].setImageDrawable(activeDot) - } - - private fun setEventListener() { - binding.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrolled( - position: Int, - positionOffset: Float, - positionOffsetPixels: Int - ) {} - - override fun onPageSelected(position: Int) { - repeat(dotCount) { - dots[it].setImageDrawable(nonActiveDot) - } - dots[position].setImageDrawable(activeDot) - } - - override fun onPageScrollStateChanged(state: Int) {} - }) - } - - private fun setObserver() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.navigateToSelectedProperty.collect { - it?.let { - navigateToProductDetail(it.id) - viewModel.displayProductDetailsComplete() - } + return ComposeView(requireContext()).apply { + setContent { + ShopScreen( + navigateToProductDetails = { id -> navigateToProductDetail(id) } - } - launch { - viewModel.currentCart.collect{} - } + ) } } } @@ -126,32 +33,4 @@ class ShopFragment : Fragment() { findNavController().navigate(direction) } - private fun addToCart(product: ProductModel) { - viewModel.addToCart(product) - showMessageSnackBar("Added ${product.name}") - } - - private fun setUpRecyclerView() { - gridListItemAdapter = GridListItemAdapter( - GridListItemAdapter.OnClickListener( - clickListener = { viewModel.displayProductDetails(it) }, - addToCartListener = { addToCart(it) } - ) - ) - binding.exclusiveOfferRecyclerview.adapter = - gridListItemAdapter - - binding.exclusiveOfferRecyclerview.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - - binding.bestSellingRecyclerview.adapter = - gridListItemAdapter - - binding.bestSellingRecyclerview.layoutManager = - LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - - binding.recommendedRecyclerview.adapter = - gridListItemAdapter - - binding.recommendedRecyclerview.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopScreen.kt new file mode 100644 index 00000000..9bf3dfaa --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopScreen.kt @@ -0,0 +1,140 @@ +package com.hieuwu.groceriesstore.presentation.shop + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material.Text +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.presentation.shop.composables.Carousel +import com.hieuwu.groceriesstore.presentation.shop.composables.ProductCatalogue +import kotlinx.coroutines.launch + +@Composable +fun ShopScreen( + modifier: Modifier = Modifier, + viewModel: ShopViewModel = hiltViewModel(), + navigateToProductDetails: (String) -> Unit +) { + + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + + ) { paddingValues -> + Box(modifier = modifier.background(colorResource(id = R.color.colorPrimary))) { + val products = viewModel.productList.collectAsState() + + Text( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + text = "Groceries Store", + color = Color.White, + style = MaterialTheme.typography.h5, + textAlign = TextAlign.Center + ) + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(top = 64.dp) + .clip(RoundedCornerShape(topStartPercent = 8, topEndPercent = 8)) + .background(Color.White) + ) { + + val selectedProduct = viewModel.navigateToSelectedProperty.collectAsState().value + LaunchedEffect(key1 = selectedProduct) { + selectedProduct?.let { + navigateToProductDetails(selectedProduct.id) + viewModel.displayProductDetailsComplete() + } + } + Image( + modifier = modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally) + .size(48.dp), + painter = painterResource(id = R.drawable.colorful_carrot), + contentDescription = null, + ) + Text( + modifier = modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.welcome_prompt) + ) + Carousel(modifier = modifier.padding(4.dp)) + ProductCatalogue( + products = products.value, + title = "Best seller", + onAddToCartClick = { product -> + viewModel.addToCart(product) + scope.launch { + snackbarHostState.showSnackbar( + "Added ${product.name}" + ) + } + }, + onNavigateToProductDetails = { id -> viewModel.displayProductDetails(id) } + ) + Spacer(modifier = modifier.height(12.dp)) + ProductCatalogue( + products = products.value, + title = "Hot deal", + onAddToCartClick = { product -> + viewModel.addToCart(product) + scope.launch { + snackbarHostState.showSnackbar( + "Added ${product.name}" + ) + } + }, + onNavigateToProductDetails = { id -> viewModel.displayProductDetails(id) } + ) + Spacer(modifier = modifier.height(12.dp)) + ProductCatalogue( + products = products.value, + title = "Exclusive offer", + onAddToCartClick = { product -> + viewModel.addToCart(product) + scope.launch { + snackbarHostState.showSnackbar( + "Added ${product.name}" + ) + } + }, + onNavigateToProductDetails = { id -> viewModel.displayProductDetails(id) } + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopViewModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopViewModel.kt index a516dbe9..cfed7ead 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopViewModel.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/ShopViewModel.kt @@ -45,6 +45,11 @@ class ShopViewModel @Inject constructor( var currentCart: StateFlow = getCurrentCart() ?.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT), null)!! + init { + viewModelScope.launch { + currentCart.collect {} + } + } fun displayProductDetails(product: ProductModel) { _navigateToSelectedProperty.value = product } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/Carousel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/Carousel.kt new file mode 100644 index 00000000..2e7c6e21 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/Carousel.kt @@ -0,0 +1,177 @@ +package com.hieuwu.groceriesstore.presentation.shop.composables + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.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.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.hieuwu.groceriesstore.R +import kotlinx.coroutines.launch + +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class) +@Composable +fun Carousel(modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxWidth() + ) { + val sectionItemListState = rememberLazyListState() + val currentVisibleIndex = remember { mutableStateOf(0) } + val coroutineScope = rememberCoroutineScope() + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + ) { + + LazyRow( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth() + .height(180.dp), + state = sectionItemListState, + ) { + items(bannerImages) { image -> + GlideImage( + contentScale = ContentScale.Crop, + model = image, + contentDescription = null, + modifier = modifier + .fillParentMaxWidth() + .animateItemPlacement() + ) + } + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier + .clip( + CircleShape + ) + .size(42.dp) + .background(colorResource(id = R.color.primary_button)) + .clickable { + coroutineScope.launch { + if (currentVisibleIndex.value != 0) { + currentVisibleIndex.value -= 1 + sectionItemListState.animateScrollToItem(currentVisibleIndex.value) + } else { + currentVisibleIndex.value = bannerImages.size - 1 + sectionItemListState.animateScrollToItem(currentVisibleIndex.value) + } + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = modifier.align(Alignment.Center), + imageVector = Icons.Filled.ArrowBackIosNew, + contentDescription = null, + tint = Color.White, + ) + } + + Box( + modifier = Modifier + .clip( + CircleShape + ) + .size(42.dp) + .background(colorResource(id = R.color.primary_button)), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = modifier + .align(Alignment.Center) + .clickable { + coroutineScope.launch { + if (currentVisibleIndex.value == bannerImages.size - 1) { + currentVisibleIndex.value = 0 + sectionItemListState.animateScrollToItem(currentVisibleIndex.value) + } else { + currentVisibleIndex.value += 1 + sectionItemListState.animateScrollToItem(currentVisibleIndex.value) + } + } + }, + imageVector = Icons.Filled.ArrowForwardIos, + contentDescription = null, + tint = Color.White, + ) + } + } + } + LazyRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + itemsIndexed(bannerImages) { curentIndex, image -> + IndicatorDot(isSelected = currentVisibleIndex.value == curentIndex) + } + } + } +} + +private val bannerImages = listOf( + "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664736648315.png?alt=media&token=2ca8be3a-37c3-4c73-b966-dcf3129958fd", + "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664737376188.png?alt=media&token=01123878-812e-4b2b-be91-d9c05b1e9b98", + "https://firebasestorage.googleapis.com/v0/b/shopee-93233.appspot.com/o/product_image1664737421330.png?alt=media&token=66f45505-ce68-4583-b018-2d2855aa714e" +) + +@Preview +@Composable +fun CarouselPreview(modifier: Modifier = Modifier) { + Carousel(modifier = modifier.fillMaxWidth()) +} + +@Composable +fun IndicatorDot( + modifier: Modifier = Modifier, + isSelected: Boolean = false +) { + Box( + modifier = modifier + .padding(4.dp) + .size(12.dp) + .clip(CircleShape) + .background( + colorResource(id = if (isSelected) R.color.primary_button else R.color.light_gray) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductCatalogue.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductCatalogue.kt new file mode 100644 index 00000000..a8277dd3 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductCatalogue.kt @@ -0,0 +1,101 @@ +package com.hieuwu.groceriesstore.presentation.shop.composables + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +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.hieuwu.groceriesstore.domain.models.ProductModel + +@Composable +fun ProductCatalogue( + modifier: Modifier = Modifier, + products: List, + title: String, + onAddToCartClick: (ProductModel) -> Unit, + onNavigateToProductDetails: (ProductModel) -> Unit +) { + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = modifier + .padding(horizontal = 12.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = title, + style = MaterialTheme.typography.subtitle1 + ) + Text(text = "Show all", modifier = modifier.clickable { + + }) + } + + LazyRow( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + contentPadding = PaddingValues(8.dp) + ) { + items(products) { item -> + ProductItem( + modifier = modifier.padding(4.dp), + product = item, + onAddToCartClick = onAddToCartClick, + onNavigateToProductDetails = { onNavigateToProductDetails(item) } + ) + } + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO +) +@Composable +fun ProductCataloguePreview(modifier: Modifier = Modifier) { + ProductCatalogue( + modifier = modifier, + title = "Best seller", + products = listOf( + ProductModel( + id = "fsdfsdds", + name = "Groeceries 1", + price = 2.4, + image = "", + ), + ProductModel( + id = "fsdfsdds", + name = "Groeceries 2", + price = 2.6, + image = "", + ), + ProductModel( + id = "fsdfsdds", + name = "Groeceries 2", + price = 2.6, + image = "", + ), + ProductModel( + id = "fsdfsdds", + name = "Groeceries 2", + price = 2.6, + image = "", + ) + ), + onAddToCartClick = {}, + onNavigateToProductDetails = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductItem.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductItem.kt new file mode 100644 index 00000000..981baa77 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/shop/composables/ProductItem.kt @@ -0,0 +1,117 @@ +package com.hieuwu.groceriesstore.presentation.shop.composables + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +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.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.domain.models.ProductModel + +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalMaterialApi::class) +@Composable +fun ProductItem( + modifier: Modifier = Modifier, + product: ProductModel, + onAddToCartClick: (ProductModel) -> Unit, + onNavigateToProductDetails: (String) -> Unit +) { + Card( + modifier = modifier + .width(180.dp) + .height(260.dp), + elevation = 2.dp, + onClick = { onNavigateToProductDetails(product.id) } + ) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(4.dp), + ) { + GlideImage( + contentScale = ContentScale.Crop, + model = product.image, + contentDescription = null, + modifier = modifier + .fillMaxWidth() + .height(80.dp) + ) + Text( + modifier = modifier.fillMaxWidth(), text = product.name ?: "", + maxLines = 1, + style = MaterialTheme.typography.subtitle1 + ) + Spacer(modifier = modifier.height(4.dp)) + Text( + modifier = modifier.fillMaxWidth(), + text = product.description ?: "", + maxLines = 2, + ) + Spacer(modifier = modifier.height(4.dp)) + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${product.price}$", + style = MaterialTheme.typography.h6 + ) + Button( + onClick = { onAddToCartClick(product) }, + modifier = Modifier.size(48.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.colorPrimary) + ) + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null, + tint = Color.White, + ) + } + } + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL, + showSystemUi = true, showBackground = false +) +@Composable +private fun ProductItemPreview(modifier: Modifier = Modifier) { + ProductItem( + modifier = modifier, + product = ProductModel( + id = "fsdfsdds", + name = "Groeceries", + price = 2.4, + image = "", + ), + onAddToCartClick = {}, + onNavigateToProductDetails = {} + ) +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cart.xml b/app/src/main/res/layout/fragment_cart.xml deleted file mode 100644 index da6f8f33..00000000 --- a/app/src/main/res/layout/fragment_cart.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -