mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-13 18:52:20 +00:00
show detail product and category
This commit is contained in:
@ -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 {
|
||||||
|
@ -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>
|
@ -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
|
|
@ -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
|
@ -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")
|
||||||
|
@ -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,
|
|
||||||
)
|
|
@ -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
|
||||||
|
)
|
@ -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
|
|
||||||
)
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
208
app/src/main/res/layout/activity_detail_product.xml
Normal file
208
app/src/main/res/layout/activity_detail_product.xml
Normal 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>
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
72
app/src/main/res/layout/item_related_product.xml
Normal file
72
app/src/main/res/layout/item_related_product.xml
Normal 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>
|
73
app/src/main/res/layout/item_review.xml
Normal file
73
app/src/main/res/layout/item_review.xml
Normal 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>
|
@ -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>
|
Reference in New Issue
Block a user