mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-12 18:22:22 +00:00
update display product
This commit is contained in:
@ -29,6 +29,9 @@
|
|||||||
android:theme="@style/Theme.Ecommerce_serang"
|
android:theme="@style/Theme.Ecommerce_serang"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.product.listproduct.ListProductActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.product.category.CategoryProductsActivity"
|
android:name=".ui.product.category.CategoryProductsActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -16,10 +16,6 @@ class HomeCategoryAdapter(
|
|||||||
private val onClick:(category:CategoryItem) -> Unit
|
private val onClick:(category:CategoryItem) -> Unit
|
||||||
): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() {
|
): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() {
|
||||||
|
|
||||||
/*
|
|
||||||
ViewHolder is responsible for caching and managing the view references for each item in
|
|
||||||
the RecyclerView.It binds the Category data to the corresponding views within the item layout.
|
|
||||||
*/
|
|
||||||
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
|
inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){
|
||||||
fun bind(category: CategoryItem) = with(binding) {
|
fun bind(category: CategoryItem) = with(binding) {
|
||||||
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
|
Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
|
||||||
@ -59,7 +55,7 @@ class HomeCategoryAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateLimitedCategory(newCategories: List<CategoryItem>){
|
fun updateLimitedCategory(newCategories: List<CategoryItem>){
|
||||||
val limitedCategories = newCategories.take(10)
|
val limitedCategories = newCategories.take(9)
|
||||||
updateData(limitedCategories)
|
updateData(limitedCategories)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,10 +14,11 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.alya.ecommerce_serang.R
|
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
|
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
|
||||||
@ -25,8 +26,8 @@ import com.alya.ecommerce_serang.ui.cart.CartActivity
|
|||||||
import com.alya.ecommerce_serang.ui.notif.NotificationActivity
|
import com.alya.ecommerce_serang.ui.notif.NotificationActivity
|
||||||
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
||||||
import com.alya.ecommerce_serang.ui.product.category.CategoryProductsActivity
|
import com.alya.ecommerce_serang.ui.product.category.CategoryProductsActivity
|
||||||
|
import com.alya.ecommerce_serang.ui.product.listproduct.ListProductActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
|
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.setLightStatusBar
|
import com.alya.ecommerce_serang.utils.setLightStatusBar
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState
|
import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState
|
||||||
@ -75,11 +76,6 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
productAdapter = HorizontalProductAdapter(
|
|
||||||
products = emptyList(),
|
|
||||||
onClick = { product -> handleProductClick(product) }
|
|
||||||
)
|
|
||||||
|
|
||||||
categoryAdapter = HomeCategoryAdapter(
|
categoryAdapter = HomeCategoryAdapter(
|
||||||
categories = emptyList(),
|
categories = emptyList(),
|
||||||
onClick = { category -> handleCategoryProduct(category)}
|
onClick = { category -> handleCategoryProduct(category)}
|
||||||
@ -87,8 +83,9 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
binding.newProducts.apply {
|
binding.newProducts.apply {
|
||||||
adapter = productAdapter
|
adapter = productAdapter
|
||||||
layoutManager = LinearLayoutManager(
|
layoutManager = GridLayoutManager(
|
||||||
context,
|
context,
|
||||||
|
2,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
@ -96,12 +93,18 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
binding.categories.apply {
|
binding.categories.apply {
|
||||||
adapter = categoryAdapter
|
adapter = categoryAdapter
|
||||||
layoutManager = LinearLayoutManager(
|
layoutManager = GridLayoutManager(
|
||||||
context,
|
context,
|
||||||
|
3,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.productshowAll.setOnClickListener {
|
||||||
|
val intent = Intent(requireContext(), ListProductActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSearchView() {
|
private fun setupSearchView() {
|
||||||
@ -155,7 +158,10 @@ class HomeFragment : Fragment() {
|
|||||||
binding.loading.root.isVisible = false
|
binding.loading.root.isVisible = false
|
||||||
binding.error.root.isVisible = false
|
binding.error.root.isVisible = false
|
||||||
binding.home.isVisible = true
|
binding.home.isVisible = true
|
||||||
productAdapter?.updateLimitedProducts(state.products)
|
val products = state.products
|
||||||
|
viewModel.loadStoresForProducts(products) // << add this here
|
||||||
|
|
||||||
|
productAdapter?.updateLimitedProducts(products)
|
||||||
}
|
}
|
||||||
is HomeUiState.Error -> {
|
is HomeUiState.Error -> {
|
||||||
binding.loading.root.isVisible = false
|
binding.loading.root.isVisible = false
|
||||||
@ -168,7 +174,9 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
@ -179,30 +187,56 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
viewModel.storeMap.collect { storeMap ->
|
||||||
|
val products = (viewModel.uiState.value as? HomeUiState.Success)?.products.orEmpty()
|
||||||
|
if (products.isNotEmpty()) {
|
||||||
|
updateProducts(products, storeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||||
|
if (products.isEmpty()) {
|
||||||
|
Log.d("HomeFragment", "Product list is empty, hiding RecyclerView")
|
||||||
|
binding.newProducts.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
Log.d("HomeFragment", "Displaying product list in RecyclerView")
|
||||||
|
binding.newProducts.visibility = View.VISIBLE // <-- Fix here
|
||||||
|
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
|
||||||
|
handleProductClick(product)
|
||||||
|
}, storeMap = storeMap)
|
||||||
|
binding.newProducts.adapter = productAdapter
|
||||||
|
productAdapter?.updateProducts(products)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initUi() {
|
private fun initUi() {
|
||||||
// For LightStatusBar
|
// For LightStatusBar
|
||||||
setLightStatusBar()
|
setLightStatusBar()
|
||||||
val banners = binding.banners
|
// val banners = binding.banners
|
||||||
banners.offscreenPageLimit = 1
|
// banners.offscreenPageLimit = 1
|
||||||
|
//
|
||||||
val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
|
// val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
|
||||||
val currentItemHorizontalMarginPx =
|
// val currentItemHorizontalMarginPx =
|
||||||
resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
|
// resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
|
||||||
val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
|
// val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
|
||||||
|
//
|
||||||
banners.setPageTransformer { page, position ->
|
// banners.setPageTransformer { page, position ->
|
||||||
page.translationX = -pageTranslationX * position
|
// page.translationX = -pageTranslationX * position
|
||||||
page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
|
// page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
banners.addItemDecoration(
|
// banners.addItemDecoration(
|
||||||
HorizontalMarginItemDecoration(
|
// HorizontalMarginItemDecoration(
|
||||||
requireContext(),
|
// requireContext(),
|
||||||
R.dimen.viewpager_current_item_horizontal_margin
|
// R.dimen.viewpager_current_item_horizontal_margin
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleProductClick(product: ProductsItem) {
|
private fun handleProductClick(product: ProductsItem) {
|
||||||
|
@ -8,15 +8,17 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
|
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
class HorizontalProductAdapter(
|
class HorizontalProductAdapter(
|
||||||
private var products: List<ProductsItem>,
|
private var products: List<ProductsItem>,
|
||||||
private val onClick: (ProductsItem) -> Unit
|
private val onClick: (ProductsItem) -> Unit,
|
||||||
|
private val storeMap: Map<Int, StoreItem>
|
||||||
) : RecyclerView.Adapter<HorizontalProductAdapter.ProductViewHolder>() {
|
) : RecyclerView.Adapter<HorizontalProductAdapter.ProductViewHolder>() {
|
||||||
|
|
||||||
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) :
|
inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(product: ProductsItem) = with(binding) {
|
fun bind(product: ProductsItem) = with(binding) {
|
||||||
@ -29,22 +31,25 @@ class HorizontalProductAdapter(
|
|||||||
|
|
||||||
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||||
|
|
||||||
itemName.text = product.name
|
tvProductName.text = product.name
|
||||||
itemPrice.text = product.price
|
tvProductPrice.text = product.price
|
||||||
rating.text = product.rating
|
rating.text = product.rating
|
||||||
|
|
||||||
// Load image using Glide
|
// Load image using Glide
|
||||||
Glide.with(itemView)
|
Glide.with(itemView)
|
||||||
.load(fullImageUrl)
|
.load(fullImageUrl)
|
||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(imageProduct)
|
.into(ivProductImage)
|
||||||
|
|
||||||
|
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||||
|
binding.tvStoreName.text = storeName
|
||||||
|
|
||||||
root.setOnClickListener { onClick(product) }
|
root.setOnClickListener { onClick(product) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||||
val binding = ItemProductHorizontalBinding.inflate(
|
val binding = ItemProductGridBinding.inflate(
|
||||||
LayoutInflater.from(parent.context), parent, false
|
LayoutInflater.from(parent.context), parent, false
|
||||||
)
|
)
|
||||||
return ProductViewHolder(binding)
|
return ProductViewHolder(binding)
|
||||||
@ -65,11 +70,11 @@ class HorizontalProductAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
|
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
|
||||||
val diffCallback = ProductDiffCallback(products, newProducts)
|
val limitedProducts = newProducts.take(10)
|
||||||
val limitedProducts = newProducts.take(10) //limit 10 produk
|
val diffCallback = ProductDiffCallback(products, limitedProducts)
|
||||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
products = limitedProducts
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
updateProducts(limitedProducts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductDiffCallback(
|
class ProductDiffCallback(
|
||||||
|
@ -117,9 +117,6 @@ class SearchHomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSearchResultsRecyclerView() {
|
private fun setupSearchResultsRecyclerView() {
|
||||||
searchResultsAdapter = SearchResultsAdapter { product ->
|
|
||||||
navigateToProductDetail(product)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchResultsRecyclerView.apply {
|
binding.searchResultsRecyclerView.apply {
|
||||||
adapter = searchResultsAdapter
|
adapter = searchResultsAdapter
|
||||||
@ -130,9 +127,26 @@ class SearchHomeFragment : Fragment() {
|
|||||||
private fun observeData() {
|
private fun observeData() {
|
||||||
viewModel.searchResults.observe(viewLifecycleOwner) { products ->
|
viewModel.searchResults.observe(viewLifecycleOwner) { products ->
|
||||||
|
|
||||||
|
if (!products.isNullOrEmpty()){
|
||||||
|
viewModel.storeDetail(products)
|
||||||
|
}
|
||||||
|
|
||||||
searchResultsAdapter?.submitList(products)
|
searchResultsAdapter?.submitList(products)
|
||||||
binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
|
binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
|
||||||
binding.searchResultsRecyclerView.isVisible = products.isNotEmpty()
|
binding.searchResultsRecyclerView.isVisible = products.isNotEmpty()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.storeDetail.observe(viewLifecycleOwner) {storeMap ->
|
||||||
|
val products = viewModel.searchResults.value.orEmpty()
|
||||||
|
if (products.isNotEmpty()){
|
||||||
|
searchResultsAdapter = SearchResultsAdapter(
|
||||||
|
onItemClick = {product -> navigateToProductDetail(product) },
|
||||||
|
storeMap = storeMap
|
||||||
|
)
|
||||||
|
binding.searchResultsRecyclerView.adapter = searchResultsAdapter
|
||||||
|
searchResultsAdapter?.submitList(products)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->
|
viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->
|
||||||
|
@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -15,6 +16,9 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
|||||||
private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList())
|
private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList())
|
||||||
val searchResults: LiveData<List<ProductsItem>> = _searchResults
|
val searchResults: LiveData<List<ProductsItem>> = _searchResults
|
||||||
|
|
||||||
|
private val _storeDetail = MutableLiveData<Map<Int, StoreItem>>()
|
||||||
|
val storeDetail : LiveData<Map<Int, StoreItem>> get() = _storeDetail
|
||||||
|
|
||||||
private val _searchHistory = MutableLiveData<List<String>>(emptyList())
|
private val _searchHistory = MutableLiveData<List<String>>(emptyList())
|
||||||
val searchHistory: LiveData<List<String>> = _searchHistory
|
val searchHistory: LiveData<List<String>> = _searchHistory
|
||||||
|
|
||||||
@ -25,10 +29,10 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
|||||||
val isSearchActive: LiveData<Boolean> = _isSearchActive
|
val isSearchActive: LiveData<Boolean> = _isSearchActive
|
||||||
|
|
||||||
fun searchProducts(query: String) {
|
fun searchProducts(query: String) {
|
||||||
Log.d("HomeViewModel", "searchProducts called with query: '$query'")
|
Log.d("SearchHomeViewModel", "searchProducts called with query: '$query'")
|
||||||
|
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
Log.d("HomeViewModel", "Query is blank, clearing results")
|
Log.d("SearchHomeViewModel", "Query is blank, clearing results")
|
||||||
_searchResults.value = emptyList()
|
_searchResults.value = emptyList()
|
||||||
_isSearchActive.value = false
|
_isSearchActive.value = false
|
||||||
return
|
return
|
||||||
@ -38,18 +42,18 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
|||||||
_isSearchActive.value = true
|
_isSearchActive.value = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Log.d("HomeViewModel", "Starting search coroutine")
|
Log.d("SearchHomeViewModeldel", "Starting search coroutine")
|
||||||
|
|
||||||
when (val result = productRepository.searchProducts(query)) {
|
when (val result = productRepository.searchProducts(query)) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
Log.d("HomeViewModel", "Search successful, found ${result.data.size} products")
|
Log.d("SearchHomeViewModel", "Search successful, found ${result.data.size} products")
|
||||||
_searchResults.postValue(result.data)
|
_searchResults.postValue(result.data)
|
||||||
|
|
||||||
// Double check the state after assignment
|
// Double check the state after assignment
|
||||||
Log.d("HomeViewModel", "Updated searchResults value has ${result.data.size} items")
|
Log.d("SearchHomeViewModel", "Updated searchResults value has ${result.data.size} items")
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
Log.e("HomeViewModel", "Search failed", result.exception)
|
Log.e("SearchHomeViewModel", "Search failed", result.exception)
|
||||||
_searchResults.postValue(emptyList())
|
_searchResults.postValue(emptyList())
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@ -58,6 +62,30 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun storeDetail(products: List<ProductsItem>){
|
||||||
|
viewModelScope.launch {
|
||||||
|
val map = mutableMapOf<Int, StoreItem>()
|
||||||
|
|
||||||
|
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||||
|
for (storeId in storeIds){
|
||||||
|
try {
|
||||||
|
when (val result = productRepository.fetchStoreDetail(storeId)){
|
||||||
|
is Result.Success -> map[storeId] = result.data
|
||||||
|
is Result.Error -> Log.e("SearchHomeViewModel", "Error Loading Store")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
} catch (e: Exception){
|
||||||
|
Log.e("SearchHomeViewModel", "Exception error for storeId: $storeId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_storeDetail.value = map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadStoreDetail(storeId: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fun clearSearch() {
|
fun clearSearch() {
|
||||||
_isSearchActive.value = false
|
_isSearchActive.value = false
|
||||||
_searchResults.value = emptyList()
|
_searchResults.value = emptyList()
|
||||||
@ -68,7 +96,7 @@ class SearchHomeViewModel (private val productRepository: ProductRepository) : V
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = productRepository.getSearchHistory()) {
|
when (val result = productRepository.getSearchHistory()) {
|
||||||
is Result.Success -> _searchHistory.value = result.data
|
is Result.Success -> _searchHistory.value = result.data
|
||||||
is Result.Error -> Log.e("HomeViewModel", "Failed to load search history", result.exception)
|
is Result.Error -> Log.e("SearchHomeViewModel", "Failed to load search history", result.exception)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,16 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
class SearchResultsAdapter(
|
class SearchResultsAdapter(
|
||||||
private val onItemClick: (ProductsItem) -> Unit
|
private val onItemClick: (ProductsItem) -> Unit,
|
||||||
|
private val storeMap: Map<Int, StoreItem>
|
||||||
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@ -43,19 +46,23 @@ class SearchResultsAdapter(
|
|||||||
|
|
||||||
fun bind(product: ProductsItem) {
|
fun bind(product: ProductsItem) {
|
||||||
binding.tvProductName.text = product.name
|
binding.tvProductName.text = product.name
|
||||||
binding.tvProductPrice.text = (product.price)
|
binding.tvProductPrice.text = product.price
|
||||||
|
|
||||||
|
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||||
|
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||||
|
} else {
|
||||||
|
product.image // Use as is if it's already a full URL
|
||||||
|
}
|
||||||
|
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||||
// Load image with Glide
|
// Load image with Glide
|
||||||
Glide.with(binding.root.context)
|
Glide.with(binding.root.context)
|
||||||
.load(product.image)
|
.load(fullImageUrl)
|
||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
// .error(R.drawable.error_image)
|
// .error(R.drawable.error_image)
|
||||||
.into(binding.ivProductImage)
|
.into(binding.ivProductImage)
|
||||||
|
|
||||||
// Set store name if available
|
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||||
product.storeId?.toString().let {
|
binding.tvStoreName.text = storeName
|
||||||
binding.tvStoreName.text = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ import com.alya.ecommerce_serang.data.repository.Result
|
|||||||
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
|
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
|
||||||
import com.alya.ecommerce_serang.ui.cart.CartActivity
|
import com.alya.ecommerce_serang.ui.cart.CartActivity
|
||||||
import com.alya.ecommerce_serang.ui.chat.ChatActivity
|
import com.alya.ecommerce_serang.ui.chat.ChatActivity
|
||||||
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
|
|
||||||
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
|
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
|
||||||
import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
|
import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
@ -45,7 +44,7 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding: ActivityDetailProductBinding
|
private lateinit var binding: ActivityDetailProductBinding
|
||||||
private lateinit var apiService: ApiService
|
private lateinit var apiService: ApiService
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
private var productAdapter: HorizontalProductAdapter? = null
|
private var productAdapter: OtherProductAdapter? = null
|
||||||
private var reviewsAdapter: ReviewsAdapter? = null
|
private var reviewsAdapter: ReviewsAdapter? = null
|
||||||
private var currentQuantity = 1
|
private var currentQuantity = 1
|
||||||
private var isWholesaleAvailable: Boolean = false
|
private var isWholesaleAvailable: Boolean = false
|
||||||
@ -125,7 +124,8 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.otherProducts.observe(this) { products ->
|
viewModel.otherProducts.observe(this) { products ->
|
||||||
updateOtherProducts(products)
|
viewModel.loadStoresForProducts(products)
|
||||||
|
// updateOtherProducts(products)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.reviewProduct.observe(this) { reviews ->
|
viewModel.reviewProduct.observe(this) { reviews ->
|
||||||
@ -155,6 +155,13 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.storeMap.observe(this){ storeMap ->
|
||||||
|
val products = viewModel.otherProducts.value.orEmpty()
|
||||||
|
if (products.isNotEmpty()) {
|
||||||
|
updateOtherProducts(products, storeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStoreInfo(store: StoreItem?) {
|
private fun updateStoreInfo(store: StoreItem?) {
|
||||||
@ -178,13 +185,21 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOtherProducts(products: List<ProductsItem>) {
|
|
||||||
|
private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||||
if (products.isEmpty()) {
|
if (products.isEmpty()) {
|
||||||
binding.recyclerViewOtherProducts.visibility = View.GONE
|
Log.d("DetailProductActivity", "Product list is empty, hiding RecyclerView")
|
||||||
|
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
|
||||||
binding.tvViewAllProducts.visibility = View.GONE
|
binding.tvViewAllProducts.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
Log.d("DetailProductActivity", "Displaying product list in RecyclerView")
|
||||||
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
|
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
|
||||||
binding.tvViewAllProducts.visibility = View.VISIBLE
|
binding.tvViewAllProducts.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
productAdapter = OtherProductAdapter(products, onClick = { product ->
|
||||||
|
handleProductClick(product)
|
||||||
|
}, storeMap = storeMap)
|
||||||
|
binding.recyclerViewOtherProducts.adapter = productAdapter
|
||||||
productAdapter?.updateProducts(products)
|
productAdapter?.updateProducts(products)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,10 +290,6 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerViewOtherProducts(){
|
private fun setupRecyclerViewOtherProducts(){
|
||||||
productAdapter = HorizontalProductAdapter(
|
|
||||||
products = emptyList(),
|
|
||||||
onClick = { productsItem -> handleProductClick(productsItem) }
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.recyclerViewOtherProducts.apply {
|
binding.recyclerViewOtherProducts.apply {
|
||||||
adapter = productAdapter
|
adapter = productAdapter
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.product
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
|
import com.alya.ecommerce_serang.R
|
||||||
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
|
import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
|
class OtherProductAdapter (
|
||||||
|
private var products: List<ProductsItem>,
|
||||||
|
private val onClick: (ProductsItem) -> Unit,
|
||||||
|
private val storeMap: Map<Int, StoreItem>
|
||||||
|
) : RecyclerView.Adapter<OtherProductAdapter.ProductViewHolder>() {
|
||||||
|
|
||||||
|
inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(product: ProductsItem) = with(binding) {
|
||||||
|
|
||||||
|
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||||
|
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||||
|
} else {
|
||||||
|
product.image // Use as is if it's already a full URL
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||||
|
|
||||||
|
tvProductName.text = product.name
|
||||||
|
tvProductPrice.text = product.price
|
||||||
|
rating.text = product.rating
|
||||||
|
|
||||||
|
// Load image using Glide
|
||||||
|
Glide.with(itemView)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.into(ivProductImage)
|
||||||
|
|
||||||
|
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||||
|
binding.tvStoreName.text = storeName
|
||||||
|
|
||||||
|
root.setOnClickListener { onClick(product) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||||
|
val binding = ItemProductHorizontalBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ProductViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = products.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
|
||||||
|
holder.bind(products[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProducts(newProducts: List<ProductsItem>) {
|
||||||
|
val diffCallback = ProductDiffCallback(products, newProducts)
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
products = newProducts
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
|
||||||
|
val limitedProducts = newProducts.take(10)
|
||||||
|
val diffCallback = ProductDiffCallback(products, limitedProducts)
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
products = limitedProducts
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductDiffCallback(
|
||||||
|
private val oldList: List<ProductsItem>,
|
||||||
|
private val newList: List<ProductsItem>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
|
||||||
|
}
|
||||||
|
}
|
@ -20,9 +20,15 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
|||||||
private val _productDetail = MutableLiveData<Product?>()
|
private val _productDetail = MutableLiveData<Product?>()
|
||||||
val productDetail: LiveData<Product?> get() = _productDetail
|
val productDetail: LiveData<Product?> get() = _productDetail
|
||||||
|
|
||||||
|
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
|
||||||
|
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
|
||||||
|
|
||||||
private val _storeDetail = MutableLiveData<Result<StoreItem>>()
|
private val _storeDetail = MutableLiveData<Result<StoreItem>>()
|
||||||
val storeDetail : LiveData<Result<StoreItem>> get() = _storeDetail
|
val storeDetail : LiveData<Result<StoreItem>> get() = _storeDetail
|
||||||
|
|
||||||
|
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
|
||||||
|
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
|
||||||
|
|
||||||
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
|
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
|
||||||
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
|
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
|
||||||
|
|
||||||
@ -59,6 +65,23 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadProductsList() {
|
||||||
|
_isLoading.value = true
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val result = repository.getAllProducts()
|
||||||
|
_productList.value = result
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error loading product list: ${e.message}")
|
||||||
|
_error.value = "Failed to load product list ${e.message}"
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadStoreDetail(storeId: Int) {
|
fun loadStoreDetail(storeId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
@ -73,6 +96,28 @@ class ProductUserViewModel(private val repository: ProductRepository) : ViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val map = mutableMapOf<Int, StoreItem>()
|
||||||
|
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||||
|
|
||||||
|
for (storeId in storeIds) {
|
||||||
|
try {
|
||||||
|
val result = repository.fetchStoreDetail(storeId)
|
||||||
|
if (result is Result.Success) {
|
||||||
|
map[storeId] = result.data
|
||||||
|
} else if (result is Result.Error) {
|
||||||
|
Log.e(TAG, "Failed to load storeId $storeId", result.exception)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception fetching storeId $storeId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeMap.postValue(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadReviews(productId: Int) {
|
fun loadReviews(productId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
|
@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.product.category
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.alya.ecommerce_serang.BuildConfig
|
import com.alya.ecommerce_serang.BuildConfig
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
@ -59,14 +58,14 @@ class ProductsCategoryAdapter(
|
|||||||
onClick(product)
|
onClick(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Show stock status
|
// // Optional: Show stock status
|
||||||
if (product.stock > 0) {
|
// if (product.stock > 0) {
|
||||||
tvStockStatus.text = "Stock: ${product.stock}"
|
// tvStockStatus.text = "Stock: ${product.stock}"
|
||||||
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
|
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
|
||||||
} else {
|
// } else {
|
||||||
tvStockStatus.text = "Out of Stock"
|
// tvStockStatus.text = "Out of Stock"
|
||||||
tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
|
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
|
import com.alya.ecommerce_serang.databinding.ActivityListProductBinding
|
||||||
|
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
||||||
|
import com.alya.ecommerce_serang.ui.product.ProductUserViewModel
|
||||||
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
|
||||||
|
class ListProductActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "ListProductActivity"
|
||||||
|
}
|
||||||
|
private lateinit var binding: ActivityListProductBinding
|
||||||
|
private lateinit var sessionManager: SessionManager
|
||||||
|
private var productAdapter: ListProductAdapter? = null
|
||||||
|
|
||||||
|
private val viewModel: ProductUserViewModel by viewModels {
|
||||||
|
BaseViewModelFactory {
|
||||||
|
val apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
val repository = ProductRepository(apiService)
|
||||||
|
ProductUserViewModel(repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityListProductBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
sessionManager = SessionManager(this)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||||
|
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
view.setPadding(
|
||||||
|
systemBars.left,
|
||||||
|
systemBars.top,
|
||||||
|
systemBars.right,
|
||||||
|
systemBars.bottom
|
||||||
|
)
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setupObserver()
|
||||||
|
setupRecyclerView()
|
||||||
|
viewModel.loadProductsList()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
|
||||||
|
binding.rvProducts.apply {
|
||||||
|
adapter = productAdapter
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
context,
|
||||||
|
2,
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "RecyclerView setup complete with GridLayoutManager")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupObserver(){
|
||||||
|
|
||||||
|
viewModel.productList.observe(this) { result ->
|
||||||
|
when (result) {
|
||||||
|
is Result.Success -> {
|
||||||
|
val products = result.data
|
||||||
|
viewModel.loadStoresForProducts(products)
|
||||||
|
Log.d(TAG, "Product list loaded successfully: ${products.size} items")
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e(TAG, "Failed to load products: ${result.exception.message}")
|
||||||
|
}
|
||||||
|
is Result.Loading -> {
|
||||||
|
// Show loading indicator if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.storeMap.observe(this){ storeMap ->
|
||||||
|
val products = (viewModel.productList.value as? Result.Success)?.data.orEmpty()
|
||||||
|
if (products.isNotEmpty()) {
|
||||||
|
updateProducts(products, storeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||||
|
if (products.isEmpty()) {
|
||||||
|
Log.d(TAG, "Product list is empty, hiding RecyclerView")
|
||||||
|
binding.rvProducts.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Displaying product list in RecyclerView")
|
||||||
|
binding.rvProducts.visibility = View.VISIBLE // <-- Fix here
|
||||||
|
productAdapter = ListProductAdapter(products, onClick = { product ->
|
||||||
|
handleProductClick(product)
|
||||||
|
}, storeMap = storeMap)
|
||||||
|
binding.rvProducts.adapter = productAdapter
|
||||||
|
productAdapter?.updateProducts(products)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleProductClick(product: ProductsItem) {
|
||||||
|
val intent = Intent(this, DetailProductActivity::class.java)
|
||||||
|
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.alya.ecommerce_serang.ui.product.listproduct
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
|
import com.alya.ecommerce_serang.R
|
||||||
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
|
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
|
class ListProductAdapter(
|
||||||
|
private var products: List<ProductsItem>,
|
||||||
|
private val onClick: (ProductsItem) -> Unit,
|
||||||
|
private val storeMap: Map<Int, StoreItem>
|
||||||
|
): RecyclerView.Adapter<ListProductAdapter.ProductViewHolder>() {
|
||||||
|
|
||||||
|
inner class ProductViewHolder(private val binding: ItemProductGridBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(product: ProductsItem) = with(binding) {
|
||||||
|
|
||||||
|
tvProductName.text = product.name
|
||||||
|
tvProductPrice.text = product.price
|
||||||
|
rating.text = product.rating
|
||||||
|
|
||||||
|
val fullImageUrl = if (product.image.startsWith("/")) {
|
||||||
|
BASE_URL + product.image.removePrefix("/") // Append base URL if the path starts with "/"
|
||||||
|
} else {
|
||||||
|
product.image // Use as is if it's already a full URL
|
||||||
|
}
|
||||||
|
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
|
||||||
|
|
||||||
|
|
||||||
|
// Load image using Glide
|
||||||
|
Glide.with(itemView)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.into(ivProductImage)
|
||||||
|
|
||||||
|
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||||
|
binding.tvStoreName.text = storeName
|
||||||
|
root.setOnClickListener { onClick(product) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
|
||||||
|
val binding = ItemProductGridBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ProductViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = products.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
|
||||||
|
holder.bind(products[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProducts(newProducts: List<ProductsItem>) {
|
||||||
|
val diffCallback = ProductDiffCallback(products, newProducts)
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
products = newProducts
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductDiffCallback(
|
||||||
|
private val oldList: List<ProductsItem>,
|
||||||
|
private val newList: List<ProductsItem>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition].id == newList[newItemPosition].id // Compare unique IDs
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition] // Compare entire object
|
||||||
|
}
|
||||||
|
}
|
@ -101,7 +101,15 @@ class StoreDetailActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.otherProducts.observe(this) { products ->
|
viewModel.otherProducts.observe(this) { products ->
|
||||||
updateOtherProducts(products)
|
viewModel.loadStoresForProducts(products)
|
||||||
|
// updateOtherProducts(products)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.storeMap.observe(this){ storeMap ->
|
||||||
|
val products = viewModel.otherProducts.value.orEmpty()
|
||||||
|
if (products.isNotEmpty()) {
|
||||||
|
updateProducts(products, storeMap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,20 +148,24 @@ class StoreDetailActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOtherProducts(products: List<ProductsItem>) {
|
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
|
||||||
if (products.isEmpty()) {
|
if (products.isEmpty()) {
|
||||||
binding.rvProducts.visibility = View.GONE
|
binding.rvProducts.visibility = View.GONE
|
||||||
|
Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView")
|
||||||
} else {
|
} else {
|
||||||
|
Log.d("StoreDetailActivity", "Displaying product list in RecyclerView")
|
||||||
|
|
||||||
binding.rvProducts.visibility = View.VISIBLE
|
binding.rvProducts.visibility = View.VISIBLE
|
||||||
|
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
|
||||||
|
handleProductClick(product)
|
||||||
|
}, storeMap = storeMap)
|
||||||
|
binding.rvProducts.adapter = productAdapter
|
||||||
productAdapter?.updateProducts(products)
|
productAdapter?.updateProducts(products)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupRecyclerViewOtherProducts(){
|
private fun setupRecyclerViewOtherProducts(){
|
||||||
productAdapter = HorizontalProductAdapter(
|
|
||||||
products = emptyList(),
|
|
||||||
onClick = { productsItem -> handleProductClick(productsItem) }
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.rvProducts.apply {
|
binding.rvProducts.apply {
|
||||||
adapter = productAdapter
|
adapter = productAdapter
|
||||||
|
@ -21,6 +21,9 @@ class StoreDetailViewModel (private val repository: ProductRepository
|
|||||||
private val _otherProducts = MutableLiveData<List<ProductsItem>>()
|
private val _otherProducts = MutableLiveData<List<ProductsItem>>()
|
||||||
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
|
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
|
||||||
|
|
||||||
|
private val _storeMap = MutableLiveData<Map<Int, StoreItem>>()
|
||||||
|
val storeMap: LiveData<Map<Int, StoreItem>> get() = _storeMap
|
||||||
|
|
||||||
private val _isLoading = MutableLiveData<Boolean>()
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
val isLoading: LiveData<Boolean> get() = _isLoading
|
val isLoading: LiveData<Boolean> get() = _isLoading
|
||||||
|
|
||||||
@ -84,4 +87,26 @@ class StoreDetailViewModel (private val repository: ProductRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val map = mutableMapOf<Int, StoreItem>()
|
||||||
|
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||||
|
|
||||||
|
for (storeId in storeIds) {
|
||||||
|
try {
|
||||||
|
val result = repository.fetchStoreDetail(storeId)
|
||||||
|
if (result is Result.Success) {
|
||||||
|
map[storeId] = result.data
|
||||||
|
} else if (result is Result.Error) {
|
||||||
|
Log.e("ProductViewModel", "Failed to load storeId $storeId", result.exception)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ProductViewModel", "Exception fetching storeId $storeId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeMap.postValue(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
|
||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -21,6 +22,9 @@ class HomeViewModel (
|
|||||||
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
|
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
|
||||||
val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow()
|
val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow()
|
||||||
|
|
||||||
|
private val _storeMap = MutableStateFlow<Map<Int, StoreItem>>(emptyMap())
|
||||||
|
val storeMap: StateFlow<Map<Int, StoreItem>> = _storeMap.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadProducts()
|
loadProducts()
|
||||||
loadCategories()
|
loadCategories()
|
||||||
@ -47,6 +51,28 @@ class HomeViewModel (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadStoresForProducts(products: List<ProductsItem>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val map = mutableMapOf<Int, StoreItem>()
|
||||||
|
val storeIds = products.mapNotNull { it.storeId }.toSet()
|
||||||
|
|
||||||
|
for (storeId in storeIds) {
|
||||||
|
try {
|
||||||
|
val result = productRepository.fetchStoreDetail(storeId)
|
||||||
|
if (result is Result.Success) {
|
||||||
|
map[storeId] = result.data
|
||||||
|
} else if (result is Result.Error) {
|
||||||
|
Log.e("HomeViewModel", "Failed to load storeId $storeId", result.exception)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("HomeViewModel", "Exception fetching storeId $storeId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeMap.value = map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun retry() {
|
fun retry() {
|
||||||
loadProducts()
|
loadProducts()
|
||||||
|
@ -487,7 +487,7 @@
|
|||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:itemCount="3"
|
tools:itemCount="3"
|
||||||
tools:listitem="@layout/item_related_product" />
|
tools:listitem="@layout/item_product_horizontal" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
39
app/src/main/res/layout/activity_list_product.xml
Normal file
39
app/src/main/res/layout/activity_list_product.xml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/Theme.Ecommerce_serang"
|
||||||
|
tools:context=".ui.product.listproduct.ListProductActivity">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/searchContainer"
|
||||||
|
layout="@layout/view_search_back"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- <com.google.android.material.divider.MaterialDivider-->
|
||||||
|
<!-- android:id="@+id/divider_product"-->
|
||||||
|
<!-- android:layout_width="match_parent"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- android:layout_marginTop="2dp"-->
|
||||||
|
<!-- app:layout_constraintTop_toBottomOf="@id/searchContainer"/>-->
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvProducts"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="23dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:spanCount="2"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:listitem="@layout/item_product_grid"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -28,15 +28,15 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<!-- <androidx.viewpager2.widget.ViewPager2-->
|
||||||
android:id="@+id/banners"
|
<!-- android:id="@+id/banners"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="132dp"
|
<!-- android:layout_height="132dp"-->
|
||||||
android:layout_marginTop="4dp"
|
<!-- android:layout_marginTop="8dp"-->
|
||||||
android:orientation="vertical"
|
<!-- android:background="@drawable/banner_default"-->
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
<!-- android:orientation="vertical"-->
|
||||||
android:background="@drawable/banner_default"
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
tools:layout_editor_absoluteX="16dp" />
|
<!-- tools:layout_editor_absoluteX="0dp" />-->
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/categoriesText"
|
android:id="@+id/categoriesText"
|
||||||
@ -49,7 +49,7 @@
|
|||||||
android:fontFamily="@font/dmsans_bold"
|
android:fontFamily="@font/dmsans_bold"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/banners" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/showAll"
|
android:id="@+id/showAll"
|
||||||
@ -72,9 +72,12 @@
|
|||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="24dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:spanCount="2"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintTop_toBottomOf="@id/categoriesText"
|
app:layout_constraintTop_toBottomOf="@id/categoriesText"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:layout_editor_absoluteX="0dp"
|
tools:layout_editor_absoluteX="0dp"
|
||||||
tools:listitem="@layout/item_category_home" />
|
tools:listitem="@layout/item_category_home" />
|
||||||
|
|
||||||
@ -110,7 +113,7 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:layout_constraintTop_toBottomOf="@id/new_products_text"
|
app:layout_constraintTop_toBottomOf="@id/new_products_text"
|
||||||
tools:itemCount="5"
|
tools:itemCount="5"
|
||||||
tools:listitem="@layout/item_section_horizontal" />
|
tools:listitem="@layout/item_section_horizontal" />
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
style="@style/Theme.Ecommerce_serang"
|
||||||
tools:context=".ui.home.SearchHomeFragment">
|
tools:context=".ui.home.SearchHomeFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -1,36 +1,55 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="88dp"
|
android:layout_width="110dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="115dp"
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<androidx.cardview.widget.CardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:id="@+id/imageLayout"
|
android:id="@+id/imageLayout"
|
||||||
app:layout_constraintDimensionRatio="1:1"
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
app:cardCornerRadius="14dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
app:strokeWidth="1dp"
|
app:strokeWidth="1dp"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
app:strokeColor="@color/gray_1">
|
app:strokeColor="@color/gray_1">
|
||||||
<ImageView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:id="@+id/image_category"
|
<ImageView
|
||||||
android:src="@drawable/makanan_ringan"
|
android:layout_width="match_parent"
|
||||||
android:scaleType="centerCrop" />
|
android:layout_height="match_parent"
|
||||||
</com.google.android.material.card.MaterialCardView>
|
android:id="@+id/image_category"
|
||||||
|
android:src="@drawable/makanan_ringan"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/tv_category"
|
||||||
|
android:backgroundTint="@color/blue_500"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
style="@style/MyCardView"
|
||||||
|
android:padding="2dp"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:strokeColor="@color/blue_500"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:lineSpacingExtra="0dp"
|
||||||
|
android:fontFamily="@font/dmsans_semibold"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="10sp"
|
||||||
|
|
||||||
|
android:text="@string/fragment_home_item_categories"/>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/name"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:text="@string/fragment_home_item_categories"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/imageLayout"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="@color/black"
|
|
||||||
android:layout_marginTop="12dp"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -57,14 +57,24 @@
|
|||||||
|
|
||||||
<!-- Stock Status -->
|
<!-- Stock Status -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_stock_status"
|
android:id="@+id/rating"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:background="@drawable/rating_background"
|
||||||
android:text="Stock: 0"
|
android:drawablePadding="4dp"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingEnd="11dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:text="@string/rating"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:fontFamily="@font/dmsans_regular"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:textColor="@color/black_200"
|
android:textAlignment="center"
|
||||||
tools:text="Stock: 15" />
|
android:gravity="center"
|
||||||
|
app:drawableStartCompat="@drawable/baseline_star_24"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_product_price" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_store_name"
|
android:id="@+id/tv_store_name"
|
||||||
@ -75,7 +85,7 @@
|
|||||||
android:fontFamily="@font/dmsans_semibold"
|
android:fontFamily="@font/dmsans_semibold"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="@color/black_200"
|
android:textColor="@color/black_200"
|
||||||
tools:text="Stock: 15" />
|
tools:text="Toko Jaya" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -1,69 +1,94 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="185dp"
|
android:layout_width="180dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_margin="8dp"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<LinearLayout
|
||||||
android:id="@+id/imageLayout"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="150dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:orientation="vertical">
|
||||||
app:cardCornerRadius="14dp"
|
|
||||||
app:layout_constraintDimensionRatio="272:218"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeColor="@color/gray_1"
|
|
||||||
app:strokeWidth="1dp">
|
|
||||||
|
|
||||||
|
<!-- Product Image -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_product"
|
android:id="@+id/iv_product_image"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="160dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:src="@color/blue1" />
|
android:background="@color/light_gray"
|
||||||
</com.google.android.material.card.MaterialCardView>
|
android:contentDescription="Product Image"
|
||||||
|
tools:src="@drawable/placeholder_image" />
|
||||||
|
|
||||||
<TextView
|
<!-- Product Info -->
|
||||||
android:id="@+id/item_name"
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="17dp"
|
android:orientation="vertical"
|
||||||
android:text="Banana"
|
android:padding="12dp">
|
||||||
android:textColor="@color/black"
|
|
||||||
android:fontFamily="@font/dmsans_medium"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/imageLayout" />
|
|
||||||
|
|
||||||
<TextView
|
<!-- Product Name -->
|
||||||
android:id="@+id/item_price"
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/tv_product_name"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:text="@string/item_price_txt"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/black"
|
android:text="Product Name"
|
||||||
android:textStyle="bold"
|
android:textSize="14sp"
|
||||||
android:textSize="16sp"
|
android:textStyle="bold"
|
||||||
app:layout_constraintTop_toBottomOf="@id/item_name" />
|
android:textColor="@color/black_500"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="Sample Product Name" />
|
||||||
|
|
||||||
<TextView
|
<!-- Product Price -->
|
||||||
android:id="@+id/rating"
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/tv_product_price"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:background="@drawable/rating_background"
|
android:layout_height="wrap_content"
|
||||||
android:drawablePadding="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:paddingStart="7dp"
|
android:text="Rp 0"
|
||||||
android:paddingTop="5dp"
|
android:textSize="16sp"
|
||||||
android:paddingEnd="11dp"
|
android:textStyle="bold"
|
||||||
android:paddingBottom="3dp"
|
android:textColor="@color/blue_500"
|
||||||
android:text="@string/rating"
|
tools:text="Rp 25,000" />
|
||||||
android:textColor="@color/black"
|
|
||||||
android:fontFamily="@font/dmsans_regular"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:gravity="center"
|
|
||||||
app:drawableStartCompat="@drawable/baseline_star_24"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/item_price" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<!-- Stock Status -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/rating_background"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingEnd="11dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:text="@string/rating"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:fontFamily="@font/dmsans_regular"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:gravity="center"
|
||||||
|
app:drawableStartCompat="@drawable/baseline_star_24"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_product_price" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_store_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Toko Jaya"
|
||||||
|
android:fontFamily="@font/dmsans_semibold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/black_200"
|
||||||
|
tools:text="Toko Jaya" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
@ -38,9 +38,10 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
app:layout_constraintTop_toBottomOf="@id/title"
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
tools:listitem="@layout/item_product_horizontal"
|
tools:listitem="@layout/item_product_grid"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -60,4 +60,17 @@
|
|||||||
<item name="cornerRadius">8dp</item>
|
<item name="cornerRadius">8dp</item>
|
||||||
<item name="backgroundTint">@color/white</item>
|
<item name="backgroundTint">@color/white</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="MyCardView" parent="@style/Widget.MaterialComponents.CardView">
|
||||||
|
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.MaterialCardView.Cut</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="ShapeAppearanceOverlay.MaterialCardView.Cut" parent="">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSizeTopRight">24dp</item>
|
||||||
|
<item name="cornerSizeTopLeft">24dp</item>
|
||||||
|
<item name="cornerSizeBottomRight">4dp</item>
|
||||||
|
<item name="cornerSizeBottomLeft">4dp</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
Reference in New Issue
Block a user