show detail product and category

This commit is contained in:
shaulascr
2025-03-15 17:42:57 +07:00
parent e8f2284a36
commit 7cca49d494
27 changed files with 673 additions and 175 deletions

View File

@ -23,7 +23,7 @@ android {
buildTypes { buildTypes {
release { release {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.3:3000/\"") buildConfigField("String", "BASE_URL", "\"http://192.168.1.4:3000/\"")
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
@ -31,7 +31,7 @@ android {
) )
} }
debug { debug {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.3:3000/\"") buildConfigField("String", "BASE_URL", "\"http://192.168.1.4:3000/\"")
} }
} }
compileOptions { compileOptions {

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -16,6 +17,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.DetailProductActivity"
android:exported="false" />
<activity <activity
android:name=".ui.auth.RegisterActivity" android:name=".ui.auth.RegisterActivity"
android:exported="true"> android:exported="true">
@ -36,9 +40,7 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:exported="true"> android:exported="true"></activity>
</activity>
</application> </application>
</manifest> </manifest>

View File

@ -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

View File

@ -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

View File

@ -14,7 +14,7 @@ data class DetailProduct(
val inStock: Int, val inStock: Int,
val price: Double, val price: Double,
val rating: Double, val rating: Double,
val related: List<Product>, val related: List<ProductsItem>,
val reviews: Int, val reviews: Int,
val title: String, val title: String,
@SerializedName("free_delivery") @SerializedName("free_delivery")

View File

@ -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,
)

View File

@ -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
)

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.data.api.response package com.alya.ecommerce_serang.data.api.response
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class AllProductResponse( data class AllProductResponse(
@ -11,47 +12,4 @@ data class AllProductResponse(
val products: List<ProductsItem> val products: List<ProductsItem>
) )
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
)

View File

@ -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<CategoryItem>,
@field:SerializedName("message")
val message: String
)

View File

@ -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.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest 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.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.LoginResponse
import com.alya.ecommerce_serang.data.api.response.OtpResponse import com.alya.ecommerce_serang.data.api.response.OtpResponse
import com.alya.ecommerce_serang.data.api.response.ProductResponse import com.alya.ecommerce_serang.data.api.response.ProductResponse
@ -32,6 +33,10 @@ interface ApiService {
@Body loginRequest: LoginRequest @Body loginRequest: LoginRequest
): Response<LoginResponse> ): Response<LoginResponse>
@GET("category")
suspend fun allCategory(
): Response<CategoryResponse>
@GET("product") @GET("product")
suspend fun getAllProduct(): Response<AllProductResponse> suspend fun getAllProduct(): Response<AllProductResponse>

View File

@ -1,7 +1,8 @@
package com.alya.ecommerce_serang.data.repository package com.alya.ecommerce_serang.data.repository
import android.util.Log 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 com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -15,7 +16,9 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
// Return a Result.Success with the list of products // Return a Result.Success with the list of products
Result.Success(response.body()?.products ?: emptyList()) Result.Success(response.body()?.products ?: emptyList())
} else { } else {
// Return a Result.Error with a custom Exception // Return a Result.Error with a custom Exception
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}")) Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
@ -26,7 +29,24 @@ class ProductRepository(private val apiService: ApiService) {
} }
} }
// suspend fun getUserData(): Response<UserResponse> { suspend fun getAllCategories(): Result<List<CategoryItem>> =
// return apiService.getProtectedData() 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)
}
}
} }

View File

@ -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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding 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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class RegisterActivity : AppCompatActivity() { class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding private lateinit var binding: ActivityRegisterBinding
private lateinit var sessionManager: SessionManager
private val registerViewModel: RegisterViewModel by viewModels{ private val registerViewModel: RegisterViewModel by viewModels{
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService() val apiService = ApiConfig.getUnauthenticatedApiService()
@ -26,6 +29,14 @@ class RegisterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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() enableEdgeToEdge()
binding = ActivityRegisterBinding.inflate(layoutInflater) binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@ -1,16 +1,19 @@
package com.alya.ecommerce_serang.ui.home package com.alya.ecommerce_serang.ui.home
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView 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.alya.ecommerce_serang.databinding.ItemCategoryHomeBinding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
class HomeCategoryAdapter( class HomeCategoryAdapter(
private val categories:List<Category>, private var categories:List<CategoryItem>,
//A lambda function that will be invoked when a category item is clicked. //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<HomeCategoryAdapter.ViewHolder>() { ): RecyclerView.Adapter<HomeCategoryAdapter.ViewHolder>() {
/* /*
@ -18,12 +21,25 @@ class HomeCategoryAdapter(
the RecyclerView.It binds the Category data to the corresponding views within the item layout. 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: Category) = with(binding){ fun bind(category: CategoryItem) = with(binding) {
Glide.with(root).load(category.image).into(image) Log.d("CategoriesAdapter", "Binding category: ${category.name}, Image: ${category.image}")
name.text = category.title
root.setOnClickListener{ val fullImageUrl = if (category.image.startsWith("/")) {
onClick(category) 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(categories[position]) holder.bind(categories[position])
} }
fun updateData(newCategories: List<CategoryItem>) {
categories = newCategories.toList()
notifyDataSetChanged()
}
fun updateLimitedCategory(newCategories: List<CategoryItem>){
val limitedCategories = newCategories.take(10)
updateData(limitedCategories)
}
} }

View File

@ -1,16 +1,20 @@
package com.alya.ecommerce_serang.ui.home package com.alya.ecommerce_serang.ui.home
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R 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.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
@ -26,6 +30,7 @@ class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var productAdapter: HorizontalProductAdapter? = null private var productAdapter: HorizontalProductAdapter? = null
private var categoryAdapter: HomeCategoryAdapter? = null
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private val viewModel: HomeViewModel by viewModels { private val viewModel: HomeViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
@ -62,6 +67,11 @@ class HomeFragment : Fragment() {
onClick = { product -> handleProductClick(product) } onClick = { product -> handleProductClick(product) }
) )
categoryAdapter = HomeCategoryAdapter(
categories = emptyList(),
onClick = { category -> handleCategoryProduct(category)}
)
binding.newProducts.apply { binding.newProducts.apply {
adapter = productAdapter adapter = productAdapter
layoutManager = LinearLayoutManager( layoutManager = LinearLayoutManager(
@ -70,10 +80,20 @@ class HomeFragment : Fragment() {
false false
) )
} }
binding.categories.apply {
adapter = categoryAdapter
layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
}
} }
private fun observeData() { private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> viewModel.uiState.collect { state ->
when (state) { when (state) {
is HomeUiState.Loading -> { is HomeUiState.Loading -> {
@ -85,7 +105,7 @@ 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?.updateProducts(state.products) productAdapter?.updateLimitedProducts(state.products)
} }
is HomeUiState.Error -> { is HomeUiState.Error -> {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
@ -101,6 +121,18 @@ class HomeFragment : Fragment() {
} }
} }
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() { private fun initUi() {
// For LightStatusBar // For LightStatusBar
setLightStatusBar() setLightStatusBar()
@ -130,16 +162,18 @@ class HomeFragment : Fragment() {
} }
private fun handleCategoryProduct(category: CategoryItem) {
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
productAdapter = null
categoryAdapter = null
_binding = null _binding = null
} }
private fun showLoading(isLoading: Boolean) { private fun showLoading(isLoading: Boolean) {
if (isLoading) { binding.progressBar.isVisible = isLoading
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
}
} }
} }

View File

@ -3,7 +3,8 @@ package com.alya.ecommerce_serang.ui.home
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.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
@ -17,25 +18,31 @@ class HomeViewModel (
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading) private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow() val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
val categories: StateFlow<List<CategoryItem>> = _categories.asStateFlow()
init { init {
loadProducts() loadProducts()
loadCategories()
} }
private fun loadProducts() { private fun loadProducts() {
viewModelScope.launch { viewModelScope.launch {
_uiState.value = HomeUiState.Loading _uiState.value = HomeUiState.Loading
when (val result = productRepository.getAllProducts()) { when (val result = productRepository.getAllProducts()) {
is Result.Success -> { is Result.Success -> _uiState.value = HomeUiState.Success(result.data)
_uiState.value = HomeUiState.Success(result.data) is Result.Error -> _uiState.value = HomeUiState.Error(result.exception.message ?: "Unknown error")
is Result.Loading -> {}
}
} }
is Result.Error -> {
_uiState.value = HomeUiState.Error(result.exception.message ?: "Unknown error")
Log.e("HomeViewModel", "Failed to fetch products", result.exception)
} }
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() { fun retry() {
loadProducts() loadProducts()
loadCategories()
} }
// private fun fetchUserData() { // private fun fetchUserData() {

View File

@ -1,9 +1,13 @@
package com.alya.ecommerce_serang.ui.home package com.alya.ecommerce_serang.ui.home
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -16,17 +20,24 @@ class HorizontalProductAdapter(
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(product: ProductsItem) = with(binding) { 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 itemName.text = product.name
itemPrice.text = product.price itemPrice.text = product.price
rating.text = product.rating rating.text = product.rating
// productSold.text = "${product.totalSold} sold"
// Load image using Glide // Load image using Glide
Glide.with(itemView) Glide.with(itemView)
// .load("${BuildConfig.BASE_URL}/product/${product.image}") .load(fullImageUrl)
// .load("${BuildConfig.BASE_URL}/${product.image}") .placeholder(R.drawable.placeholder_image)
.load(product.image) .into(imageProduct)
.into(image)
root.setOnClickListener { onClick(product) } root.setOnClickListener { onClick(product) }
} }
@ -46,7 +57,32 @@ class HorizontalProductAdapter(
} }
fun updateProducts(newProducts: List<ProductsItem>) { fun updateProducts(newProducts: List<ProductsItem>) {
val diffCallback = ProductDiffCallback(products, newProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback)
products = newProducts products = newProducts
notifyDataSetChanged() diffResult.dispatchUpdatesTo(this)
}
fun updateLimitedProducts(newProducts: List<ProductsItem>) {
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<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
} }
} }

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -1,12 +1,12 @@
package com.alya.ecommerce_serang.utils package com.alya.ecommerce_serang.utils
import android.os.Parcelable 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 import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class ProductQuery ( data class ProductQuery (
val category: Category? = null, val category: CategoryItem,
val search:String? = null, val search:String? = null,
val range:Pair<Float,Float> = 0f to 10000f, val range:Pair<Float,Float> = 0f to 10000f,
val rating:Int? = null, val rating:Int? = null,

View File

@ -19,7 +19,7 @@ class SessionManager(context: Context) {
} }
fun getToken(): String? { fun getToken(): String? {
val token = sharedPreferences.getString("auth_token", null) val token = sharedPreferences.getString(USER_TOKEN, null)
Log.d("SessionManager", "Retrieved token: $token") Log.d("SessionManager", "Retrieved token: $token")
return token return token
} }

View File

@ -0,0 +1,208 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".ui.product.DetailProductActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
app:titleTextColor="@android:color/black"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:title="Detail Produk" />
<!-- Main Content with Scroll -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="60dp"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@id/bottom_buttons">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Product Image -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<ImageView
android:id="@+id/imgProduct"
android:layout_width="match_parent"
android:layout_height="220dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" />
</androidx.cardview.widget.CardView>
<!-- Product Info -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
android:padding="12dp">
<!-- Sold & Rating -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tvProductPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65.000"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/black" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Keripik Ikan Tenggiri"
android:textSize="16sp"
android:textColor="@color/black" />
<TextView
android:id="@+id/tvSold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Terjual 10 buah"
android:textColor="@color/gray_1" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:src="@drawable/baseline_star_24" />
<TextView
android:id="@+id/tvRating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="4.5"
android:textColor="@color/black" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Buyer Reviews -->
<TextView
android:text="Ulasan Pembeli"
android:textSize="16sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvReviews"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
<!-- Product Details -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
android:padding="12dp">
<TextView
android:text="Detail Produk"
android:textSize="16sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="Berat: 200 gram"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="Stok: 100 buah"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="Kategori: Makanan Ringan"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
<!-- Related Products -->
<TextView
android:text="Produk lainnya"
android:textSize="16sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRelatedProducts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:nestedScrollingEnabled="false" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- Fixed Bottom Buttons -->
<FrameLayout
android:id="@+id/bottom_buttons"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp">
<Button
android:id="@+id/btnAddToCart"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Keranjang"
android:backgroundTint="@color/soft_gray"
android:textColor="@color/black" />
<Button
android:id="@+id/btnBuyNow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Beli Sekarang"
android:backgroundTint="@color/blue_500"
android:textColor="@color/white" />
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -92,7 +92,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/new_products_text" android:text="@string/sold_product_text"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="22sp" android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -17,7 +17,7 @@
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/image" android:id="@+id/image_category"
android:src="@drawable/makanan_ringan" android:src="@drawable/makanan_ringan"
android:scaleType="centerCrop" /> android:scaleType="centerCrop" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@ -17,7 +17,7 @@
app:strokeWidth="1dp"> app:strokeWidth="1dp">
<ImageView <ImageView
android:id="@+id/image" android:id="@+id/image_product"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="centerCrop" android:scaleType="centerCrop"

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:gravity="center_horizontal">
<!-- Product Image -->
<ImageView
android:id="@+id/imgRelatedProduct"
android:layout_width="140dp"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" />
<!-- Product Name -->
<TextView
android:id="@+id/tvRelatedProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Keripik Kulit Sapi"
android:textSize="14sp"
android:textStyle="bold"
android:gravity="fill"
android:layout_marginTop="8dp"
android:textColor="@color/black" />
<!-- Price -->
<TextView
android:id="@+id/tvRelatedProductPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Rp45.000"
android:textSize="14sp"
android:textStyle="bold"
android:gravity="fill"
android:layout_marginTop="4dp"
android:textColor="@color/black" />
<!-- Rating -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_gravity="start"
android:layout_marginTop="4dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/baseline_star_24" />
<TextView
android:id="@+id/tvRelatedProductRating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5.0"
android:textColor="@color/black" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<!-- User Profile Image -->
<ImageView
android:id="@+id/imgUser"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image"
android:background="@drawable/placeholder_image" />
<!-- Review Content -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<!-- Username & Rating -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="budi21"
android:textStyle="bold"
android:textColor="@color/black" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:src="@drawable/baseline_star_24" />
<TextView
android:id="@+id/tvReviewRating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5.0"
android:textColor="@color/black" />
</LinearLayout>
<!-- Review Text -->
<TextView
android:id="@+id/tvReviewText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enak sekali dan renyah. Sudah dua kali pesan. Terima kasih."
android:textColor="@color/black"
android:layout_marginTop="4dp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -46,4 +46,5 @@
<string name="hint_login_password">Kata Sandi</string> <string name="hint_login_password">Kata Sandi</string>
<string name="forget_password">Lupa Kata Sandi?</string> <string name="forget_password">Lupa Kata Sandi?</string>
<string name="enter_otp">Masukkan Kode OTP</string> <string name="enter_otp">Masukkan Kode OTP</string>
<string name="sold_product_text">Produk Terlaris</string>
</resources> </resources>