From 7cca49d49471a96a0d34d1036f8c9982ec3a44e7 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Sat, 15 Mar 2025 17:42:57 +0700 Subject: [PATCH] show detail product and category --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 8 +- .../ecommerce_serang/data/api/dto/Category.kt | 11 - .../data/api/dto/CategoryItem.kt | 22 ++ .../data/api/dto/DetailProduct.kt | 2 +- .../ecommerce_serang/data/api/dto/Product.kt | 17 -- .../data/api/dto/ProductsItem.kt | 49 +++++ .../data/api/response/AllProductResponse.kt | 44 +--- .../data/api/response/CategoryResponse.kt | 15 ++ .../data/api/retrofit/ApiService.kt | 5 + .../data/repository/ProductRepository.kt | 28 ++- .../ui/auth/RegisterActivity.kt | 11 + .../ui/home/HomeCategoryAdapter.kt | 42 +++- .../ecommerce_serang/ui/home/HomeFragment.kt | 86 +++++--- .../ecommerce_serang/ui/home/HomeViewModel.kt | 32 ++- .../ui/home/HorizontalProductAdapter.kt | 50 ++++- .../ui/product/DetailProductActivity.kt | 21 ++ .../ui/product/ProductViewHolder.kt | 35 --- .../ecommerce_serang/utils/ProductQuery.kt | 4 +- .../ecommerce_serang/utils/SessionManager.kt | 2 +- .../res/layout/activity_detail_product.xml | 208 ++++++++++++++++++ app/src/main/res/layout/fragment_home.xml | 2 +- .../main/res/layout/item_category_home.xml | 2 +- .../res/layout/item_product_horizontal.xml | 2 +- .../main/res/layout/item_related_product.xml | 72 ++++++ app/src/main/res/layout/item_review.xml | 73 ++++++ app/src/main/res/values/strings.xml | 1 + 27 files changed, 673 insertions(+), 175 deletions(-) delete mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CategoryItem.kt delete mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ProductsItem.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/CategoryResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt delete mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt create mode 100644 app/src/main/res/layout/activity_detail_product.xml create mode 100644 app/src/main/res/layout/item_related_product.xml create mode 100644 app/src/main/res/layout/item_review.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc9e2b6..2e2e461 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,7 +23,7 @@ android { buildTypes { release { - buildConfigField("String", "BASE_URL", "\"http://192.168.1.3:3000/\"") + buildConfigField("String", "BASE_URL", "\"http://192.168.1.4:3000/\"") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -31,7 +31,7 @@ android { ) } debug { - buildConfigField("String", "BASE_URL", "\"http://192.168.1.3:3000/\"") + buildConfigField("String", "BASE_URL", "\"http://192.168.1.4:3000/\"") } } compileOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8986548..ac53989 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + + @@ -36,9 +40,7 @@ android:exported="false" /> - - + android:exported="true"> \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt deleted file mode 100644 index 95fe0bc..0000000 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.alya.ecommerce_serang.data.api.dto - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Category( - val id: String, - val image: String, - val title: String -): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CategoryItem.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CategoryItem.kt new file mode 100644 index 0000000..ca0963d --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CategoryItem.kt @@ -0,0 +1,22 @@ +package com.alya.ecommerce_serang.data.api.dto + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize + +data class CategoryItem( + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("store_type_id") + val storeTypeId: Int +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt index fe2355f..0680a18 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt @@ -14,7 +14,7 @@ data class DetailProduct( val inStock: Int, val price: Double, val rating: Double, - val related: List, + val related: List, val reviews: Int, val title: String, @SerializedName("free_delivery") diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt deleted file mode 100644 index 4af2851..0000000 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.alya.ecommerce_serang.data.api.dto - - -import com.google.gson.annotations.SerializedName - -data class Product ( - val id: String, - val discount: Double?, - @SerializedName("favorite") - var wishlist: Boolean, - val image: String, - val price: Double, - val rating: Double, - @SerializedName("rating_count") - val ratingCount: Int, - val title: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ProductsItem.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ProductsItem.kt new file mode 100644 index 0000000..37a5bf2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ProductsItem.kt @@ -0,0 +1,49 @@ +package com.alya.ecommerce_serang.data.api.dto + + +import com.google.gson.annotations.SerializedName + +data class ProductsItem( + + @field:SerializedName("store_id") + val storeId: Int, + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("rating") + val rating: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("weight") + val weight: Int, + + @field:SerializedName("is_pre_order") + val isPreOrder: Boolean, + + @field:SerializedName("category_id") + val categoryId: Int, + + @field:SerializedName("price") + val price: String, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("min_order") + val minOrder: Int, + + @field:SerializedName("total_sold") + val totalSold: Int, + + @field:SerializedName("stock") + val stock: Int, + + @field:SerializedName("status") + val status: String +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt index f4fd1a5..e45093f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt @@ -1,5 +1,6 @@ package com.alya.ecommerce_serang.data.api.response +import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.google.gson.annotations.SerializedName data class AllProductResponse( @@ -11,47 +12,4 @@ data class AllProductResponse( val products: List ) -data class ProductsItem( - @field:SerializedName("store_id") - val storeId: Int, - - @field:SerializedName("image") - val image: String, - - @field:SerializedName("rating") - val rating: String, - - @field:SerializedName("description") - val description: String, - - @field:SerializedName("weight") - val weight: Int, - - @field:SerializedName("is_pre_order") - val isPreOrder: Boolean, - - @field:SerializedName("category_id") - val categoryId: Int, - - @field:SerializedName("price") - val price: String, - - @field:SerializedName("name") - val name: String, - - @field:SerializedName("id") - val id: Int, - - @field:SerializedName("min_order") - val minOrder: Int, - - @field:SerializedName("total_sold") - val totalSold: Int, - - @field:SerializedName("stock") - val stock: Int, - - @field:SerializedName("status") - val status: String -) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/CategoryResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/CategoryResponse.kt new file mode 100644 index 0000000..26867ca --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/CategoryResponse.kt @@ -0,0 +1,15 @@ +package com.alya.ecommerce_serang.data.api.response + +import com.alya.ecommerce_serang.data.api.dto.CategoryItem +import com.google.gson.annotations.SerializedName + +data class CategoryResponse( + + @field:SerializedName("Category") + val category: List, + + @field:SerializedName("message") + val message: String +) + + diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 7981961..0d4a120 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -4,6 +4,7 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.response.AllProductResponse +import com.alya.ecommerce_serang.data.api.response.CategoryResponse import com.alya.ecommerce_serang.data.api.response.LoginResponse import com.alya.ecommerce_serang.data.api.response.OtpResponse import com.alya.ecommerce_serang.data.api.response.ProductResponse @@ -32,6 +33,10 @@ interface ApiService { @Body loginRequest: LoginRequest ): Response + @GET("category") + suspend fun allCategory( + ): Response + @GET("product") suspend fun getAllProduct(): Response diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index d43462f..cfe5e8a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -1,7 +1,8 @@ package com.alya.ecommerce_serang.data.repository import android.util.Log -import com.alya.ecommerce_serang.data.api.response.ProductsItem +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.retrofit.ApiService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -15,7 +16,9 @@ class ProductRepository(private val apiService: ApiService) { if (response.isSuccessful) { // Return a Result.Success with the list of products + Result.Success(response.body()?.products ?: emptyList()) + } else { // Return a Result.Error with a custom Exception Result.Error(Exception("Failed to fetch products. Code: ${response.code()}")) @@ -26,7 +29,24 @@ class ProductRepository(private val apiService: ApiService) { } } -// suspend fun getUserData(): Response { -// return apiService.getProtectedData() -// } + suspend fun getAllCategories(): Result> = + withContext(Dispatchers.IO) { + try { + Log.d("Categories", "Attempting to fetch categories") + val response = apiService.allCategory() + + if (response.isSuccessful) { + val categories = response.body()?.category ?: emptyList() + Log.d("Categories", "Fetched categories: $categories") + categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") } + Result.Success(categories) + } else { + Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}")) + } + } catch (e: Exception) { + Log.e("Categories", "Error fetching categories", e) + Result.Error(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index 5a10983..f9205f7 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -12,10 +12,13 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding +import com.alya.ecommerce_serang.ui.MainActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager class RegisterActivity : AppCompatActivity() { private lateinit var binding: ActivityRegisterBinding + private lateinit var sessionManager: SessionManager private val registerViewModel: RegisterViewModel by viewModels{ BaseViewModelFactory { val apiService = ApiConfig.getUnauthenticatedApiService() @@ -26,6 +29,14 @@ class RegisterActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + sessionManager = SessionManager(this) + if (!sessionManager.getToken().isNullOrEmpty()) { + // User already logged in, redirect to MainActivity + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + enableEdgeToEdge() binding = ActivityRegisterBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt index a2b2009..0e9a0bd 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt @@ -1,16 +1,19 @@ package com.alya.ecommerce_serang.ui.home +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.alya.ecommerce_serang.data.api.dto.Category +import com.alya.ecommerce_serang.BuildConfig.BASE_URL +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.CategoryItem import com.alya.ecommerce_serang.databinding.ItemCategoryHomeBinding import com.bumptech.glide.Glide class HomeCategoryAdapter( - private val categories:List, + private var categories:List, //A lambda function that will be invoked when a category item is clicked. - private val onClick:(category:Category) -> Unit + private val onClick:(category:CategoryItem) -> Unit ): RecyclerView.Adapter() { /* @@ -18,12 +21,25 @@ class HomeCategoryAdapter( 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){ - fun bind(category: Category) = with(binding){ - Glide.with(root).load(category.image).into(image) - name.text = category.title - root.setOnClickListener{ - onClick(category) + fun bind(category: CategoryItem) = with(binding) { + Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}") + + val fullImageUrl = if (category.image.startsWith("/")) { + BASE_URL + category.image.removePrefix("/") // Append base URL if the path starts with "/" + } else { + category.image // Use as is if it's already a full URL } + + Log.d("CategoriesAdapter", "Loading image: $fullImageUrl") + + Glide.with(itemView.context) + .load(fullImageUrl) // Ensure full URL + .placeholder(R.drawable.placeholder_image) + .into(imageCategory) + + name.text = category.name + + root.setOnClickListener { onClick(category) } } } @@ -36,4 +52,14 @@ class HomeCategoryAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(categories[position]) } + + fun updateData(newCategories: List) { + categories = newCategories.toList() + notifyDataSetChanged() + } + + fun updateLimitedCategory(newCategories: List){ + val limitedCategories = newCategories.take(10) + updateData(limitedCategories) + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index 08db098..902ab06 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -1,16 +1,20 @@ package com.alya.ecommerce_serang.ui.home import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.data.api.response.ProductsItem +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.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.databinding.FragmentHomeBinding @@ -26,6 +30,7 @@ class HomeFragment : Fragment() { private var _binding: FragmentHomeBinding? = null private val binding get() = _binding!! private var productAdapter: HorizontalProductAdapter? = null + private var categoryAdapter: HomeCategoryAdapter? = null private lateinit var sessionManager: SessionManager private val viewModel: HomeViewModel by viewModels { BaseViewModelFactory { @@ -62,6 +67,11 @@ class HomeFragment : Fragment() { onClick = { product -> handleProductClick(product) } ) + categoryAdapter = HomeCategoryAdapter( + categories = emptyList(), + onClick = { category -> handleCategoryProduct(category)} + ) + binding.newProducts.apply { adapter = productAdapter layoutManager = LinearLayoutManager( @@ -70,37 +80,59 @@ class HomeFragment : Fragment() { false ) } + + binding.categories.apply { + adapter = categoryAdapter + layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + } } private fun observeData() { viewLifecycleOwner.lifecycleScope.launch { - viewModel.uiState.collect { state -> - when (state) { - is HomeUiState.Loading -> { - binding.loading.root.isVisible = true - binding.error.root.isVisible = false - binding.home.isVisible = false - } - is HomeUiState.Success -> { - binding.loading.root.isVisible = false - binding.error.root.isVisible = false - binding.home.isVisible = true - productAdapter?.updateProducts(state.products) - } - is HomeUiState.Error -> { - binding.loading.root.isVisible = false - binding.error.root.isVisible = true - binding.home.isVisible = false - binding.error.errorMessage.text = state.message - binding.error.retryButton.setOnClickListener { - viewModel.retry() + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + when (state) { + is HomeUiState.Loading -> { + binding.loading.root.isVisible = true + binding.error.root.isVisible = false + binding.home.isVisible = false + } + is HomeUiState.Success -> { + binding.loading.root.isVisible = false + binding.error.root.isVisible = false + binding.home.isVisible = true + productAdapter?.updateLimitedProducts(state.products) + } + is HomeUiState.Error -> { + binding.loading.root.isVisible = false + binding.error.root.isVisible = true + binding.home.isVisible = false + binding.error.errorMessage.text = state.message + binding.error.retryButton.setOnClickListener { + viewModel.retry() + } } } } } } + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.categories.collect { categories -> + Log.d("Categories", "Updated Categories: $categories") + categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") } + categoryAdapter?.updateLimitedCategory(categories) + } + } + } } + private fun initUi() { // For LightStatusBar setLightStatusBar() @@ -130,16 +162,18 @@ class HomeFragment : Fragment() { } + private fun handleCategoryProduct(category: CategoryItem) { + + } + override fun onDestroyView() { super.onDestroyView() + productAdapter = null + categoryAdapter = null _binding = null } private fun showLoading(isLoading: Boolean) { - if (isLoading) { - binding.progressBar.visibility = View.VISIBLE - } else { - binding.progressBar.visibility = View.GONE - } + binding.progressBar.isVisible = isLoading } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt index 14e81af..265ea4d 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt @@ -3,7 +3,8 @@ package com.alya.ecommerce_serang.ui.home import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.alya.ecommerce_serang.data.api.response.ProductsItem +import com.alya.ecommerce_serang.data.api.dto.CategoryItem +import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.Result import kotlinx.coroutines.flow.MutableStateFlow @@ -17,25 +18,31 @@ class HomeViewModel ( private val _uiState = MutableStateFlow(HomeUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() + private val _categories = MutableStateFlow>(emptyList()) + val categories: StateFlow> = _categories.asStateFlow() + init { loadProducts() + loadCategories() } private fun loadProducts() { viewModelScope.launch { _uiState.value = HomeUiState.Loading - when (val result = productRepository.getAllProducts()) { - is Result.Success -> { - _uiState.value = HomeUiState.Success(result.data) - } - is Result.Error -> { - _uiState.value = HomeUiState.Error(result.exception.message ?: "Unknown error") - Log.e("HomeViewModel", "Failed to fetch products", result.exception) - } - is Result.Loading-> { + is Result.Success -> _uiState.value = HomeUiState.Success(result.data) + is Result.Error -> _uiState.value = HomeUiState.Error(result.exception.message ?: "Unknown error") + is Result.Loading -> {} + } + } + } - } + private fun loadCategories() { + viewModelScope.launch { + when (val result = productRepository.getAllCategories()) { + is Result.Success -> _categories.value = result.data + is Result.Error -> Log.e("HomeViewModel", "Failed to fetch categories", result.exception) + is Result.Loading -> {} } } } @@ -43,6 +50,7 @@ class HomeViewModel ( fun retry() { loadProducts() + loadCategories() } // private fun fetchUserData() { @@ -67,4 +75,4 @@ sealed class HomeUiState { object Loading : HomeUiState() data class Success(val products: List) : HomeUiState() data class Error(val message: String) : HomeUiState() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt index 61c48b8..8af4b5f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt @@ -1,9 +1,13 @@ package com.alya.ecommerce_serang.ui.home +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.data.api.response.ProductsItem +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.databinding.ItemProductHorizontalBinding import com.bumptech.glide.Glide @@ -16,17 +20,24 @@ class HorizontalProductAdapter( 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") + itemName.text = product.name itemPrice.text = product.price rating.text = product.rating -// productSold.text = "${product.totalSold} sold" // Load image using Glide Glide.with(itemView) -// .load("${BuildConfig.BASE_URL}/product/${product.image}") -// .load("${BuildConfig.BASE_URL}/${product.image}") - .load(product.image) - .into(image) + .load(fullImageUrl) + .placeholder(R.drawable.placeholder_image) + .into(imageProduct) root.setOnClickListener { onClick(product) } } @@ -46,7 +57,32 @@ class HorizontalProductAdapter( } fun updateProducts(newProducts: List) { + val diffCallback = ProductDiffCallback(products, newProducts) + val diffResult = DiffUtil.calculateDiff(diffCallback) products = newProducts - notifyDataSetChanged() + diffResult.dispatchUpdatesTo(this) + } + + fun updateLimitedProducts(newProducts: List) { + val diffCallback = ProductDiffCallback(products, newProducts) + val limitedProducts = newProducts.take(10) // Limit to 10 items + val diffResult = DiffUtil.calculateDiff(diffCallback) + diffResult.dispatchUpdatesTo(this) + updateProducts(limitedProducts) + } + + class ProductDiffCallback( + private val oldList: List, + private val newList: List + ) : 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt new file mode 100644 index 0000000..284f79d --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -0,0 +1,21 @@ +package com.alya.ecommerce_serang.ui.product + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.alya.ecommerce_serang.R + +class DetailProductActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_detail_product) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt deleted file mode 100644 index 43a7e0e..0000000 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.alya.ecommerce_serang.ui.product - -import androidx.recyclerview.widget.RecyclerView -import com.alya.ecommerce_serang.R -import com.alya.ecommerce_serang.data.api.dto.Product -import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding -import com.bumptech.glide.Glide - -class ProductViewHolder(private val binding: ItemProductHorizontalBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind( - product: Product, - onClick: (product: Product) -> Unit, - ) = with(binding) { - Glide.with(root).load(product.image).into(image) - -// discount.isVisible = product.discount != null -// product.discount?.let { -// val discount = (product.discount / product.price * 100).roundToInt() -// binding.discount.text = -// root.context.getString(R.string.fragment_item_product_discount, discount) -// } - - itemName.text = product.title - rating.text = String.format("%.1f", product.rating) - -// val current = product.price - (product.discount ?: 0.0) - val current = product.price - itemPrice.text = root.context.getString(R.string.item_price_txt, current) - - root.setOnClickListener { - onClick(product) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt index 83f557e..e0cae06 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt @@ -1,12 +1,12 @@ package com.alya.ecommerce_serang.utils import android.os.Parcelable -import com.alya.ecommerce_serang.data.api.dto.Category +import com.alya.ecommerce_serang.data.api.dto.CategoryItem import kotlinx.parcelize.Parcelize @Parcelize data class ProductQuery ( - val category: Category? = null, + val category: CategoryItem, val search:String? = null, val range:Pair = 0f to 10000f, val rating:Int? = null, diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt index 52b405b..6feaa5c 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt @@ -19,7 +19,7 @@ class SessionManager(context: Context) { } fun getToken(): String? { - val token = sharedPreferences.getString("auth_token", null) + val token = sharedPreferences.getString(USER_TOKEN, null) Log.d("SessionManager", "Retrieved token: $token") return token } diff --git a/app/src/main/res/layout/activity_detail_product.xml b/app/src/main/res/layout/activity_detail_product.xml new file mode 100644 index 0000000..433367a --- /dev/null +++ b/app/src/main/res/layout/activity_detail_product.xml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +