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

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
@ -16,6 +17,9 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.product.DetailProductActivity"
android:exported="false" />
<activity
android:name=".ui.auth.RegisterActivity"
android:exported="true">
@ -36,9 +40,7 @@
android:exported="false" />
<activity
android:name=".ui.MainActivity"
android:exported="true">
</activity>
android:exported="true"></activity>
</application>
</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 price: Double,
val rating: Double,
val related: List<Product>,
val related: List<ProductsItem>,
val reviews: Int,
val title: String,
@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
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<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.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<LoginResponse>
@GET("category")
suspend fun allCategory(
): Response<CategoryResponse>
@GET("product")
suspend fun getAllProduct(): Response<AllProductResponse>

View File

@ -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<UserResponse> {
// return apiService.getProtectedData()
// }
suspend fun getAllCategories(): Result<List<CategoryItem>> =
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.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)

View File

@ -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<Category>,
private var categories:List<CategoryItem>,
//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>() {
/*
@ -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<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
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
}
}

View File

@ -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>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
private val _categories = MutableStateFlow<List<CategoryItem>>(emptyList())
val categories: StateFlow<List<CategoryItem>> = _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<ProductsItem>) : HomeUiState()
data class Error(val message: String) : HomeUiState()
}
}

View File

@ -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<ProductsItem>) {
val diffCallback = ProductDiffCallback(products, newProducts)
val diffResult = DiffUtil.calculateDiff(diffCallback)
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
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<Float,Float> = 0f to 10000f,
val rating:Int? = null,

View File

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

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_marginStart="32dp"
android:layout_marginTop="24dp"
android:text="@string/new_products_text"
android:text="@string/sold_product_text"
android:textColor="@color/black"
android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent"

View File

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

View File

@ -17,7 +17,7 @@
app:strokeWidth="1dp">
<ImageView
android:id="@+id/image"
android:id="@+id/image_product"
android:layout_width="match_parent"
android:layout_height="match_parent"
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="forget_password">Lupa Kata Sandi?</string>
<string name="enter_otp">Masukkan Kode OTP</string>
<string name="sold_product_text">Produk Terlaris</string>
</resources>