add reviews

This commit is contained in:
shaulascr
2025-03-26 19:28:14 +07:00
parent 6239745d82
commit 055f88977b
11 changed files with 330 additions and 12 deletions

View File

@ -16,7 +16,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31" >
tools:targetApi="31">
<activity
android:name=".ui.product.ReviewProductActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.balance.BalanceActivity"
android:exported="false" />
@ -58,8 +61,7 @@
android:exported="false" />
<activity
android:name=".ui.MainActivity"
android:exported="false">
</activity>
android:exported="false"></activity>
</application>
</manifest>

View File

@ -20,7 +20,7 @@ data class ReviewsItem(
val reviewDate: String,
@field:SerializedName("user_image")
val userImage: Any,
val userImage: String? = null,
@field:SerializedName("product_id")
val productId: Int,

View File

@ -5,6 +5,7 @@ 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.DetailStoreProductResponse
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
@ -55,6 +56,11 @@ interface ApiService {
@GET("profile")
suspend fun getUserProfile(): Response<ProfileResponse>
@GET("store/detail/{id}")
suspend fun getDetailStore (
@Path("id") storeId: Int
): Response<DetailStoreProductResponse>
@GET("mystore")
fun getStore (): Call<StoreResponse>

View File

@ -23,6 +23,7 @@ class ProductRepository(private val apiService: ApiService) {
} else {
// Return a Result.Error with a custom Exception
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
}
} catch (e: Exception) {
@ -78,5 +79,19 @@ class ProductRepository(private val apiService: ApiService) {
null
}
}
}
}
// suspend fun fetchStoreDetail(storeId: Int): Store? {
// return try {
// val response = apiService.getStore(storeId)
// if (response.isSucessful) {
// response.body()?.store
// } else {
// Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
//
// null
// }
// } catch (e: Exception) {
// null
// }
// }

View File

@ -1,16 +1,21 @@
package com.alya.ecommerce_serang.ui.product
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.Product
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
@ -19,6 +24,8 @@ class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var productAdapter: HorizontalProductAdapter? = null
private var reviewsAdapter: ReviewsAdapter? = null
private val viewModel: ProductViewModel by viewModels {
BaseViewModelFactory {
@ -36,19 +43,15 @@ class DetailProductActivity : AppCompatActivity() {
apiService = ApiConfig.getApiService(sessionManager)
val productId = intent.getIntExtra("PRODUCT_ID", -1)
//nanti tambah get store id dari HomeFragment Product.storeId
if (productId == -1) {
Log.e("DetailProductActivity", "Invalid Product ID")
finish() // Close activity if no valid ID
return
}
// 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
// }
viewModel.loadProductDetail(productId)
viewModel.loadReviews(productId)
viewModel.productDetail.observe(this) { product ->
if (product != null) {
@ -61,6 +64,7 @@ class DetailProductActivity : AppCompatActivity() {
}
}
observeProductDetail()
observeProductReviews()
}
private fun observeProductDetail() {
viewModel.productDetail.observe(this) { product ->
@ -68,6 +72,12 @@ class DetailProductActivity : AppCompatActivity() {
}
}
private fun observeProductReviews() {
viewModel.reviewProduct.observe(this) { reviews ->
setupRecyclerViewReviewsProduct(reviews)
}
}
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = product.price
@ -79,6 +89,10 @@ class DetailProductActivity : AppCompatActivity() {
binding.tvDescription.text = product.description
binding.tvSellerName.text = product.storeId.toString()
binding.tvViewAllReviews.setOnClickListener{
handleAllReviewsClick(product.productId)
}
val fullImageUrl = when (val img = product.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
@ -91,5 +105,52 @@ class DetailProductActivity : AppCompatActivity() {
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage)
setupRecyclerViewOtherProducts()
}
private fun handleAllReviewsClick(productId: Int) {
val intent = Intent(this, ReviewProductActivity::class.java)
intent.putExtra("PRODUCT_ID", productId) // Pass product ID
startActivity(intent)
}
private fun setupRecyclerViewOtherProducts(){
productAdapter = HorizontalProductAdapter(
products = emptyList(),
onClick = { productsItem -> handleProductClick(productsItem) }
)
binding.recyclerViewOtherProducts.apply {
adapter = productAdapter
layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
}
}
private fun setupRecyclerViewReviewsProduct(reviewList: List<ReviewsItem>){
val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList()
reviewsAdapter = ReviewsAdapter(
reviewList = limitedReviewList
)
binding.recyclerViewReviews.apply {
adapter = reviewsAdapter
layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
}
}
private fun handleProductClick(product: ProductsItem) {
val intent = Intent(this, DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
startActivity(intent)
}
}

View File

@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.Product
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.Store
import com.alya.ecommerce_serang.data.repository.ProductRepository
import kotlinx.coroutines.launch
@ -13,10 +15,30 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
private val _storeDetail = MutableLiveData<Store?>()
val storeDetail : LiveData<Store?> get() = _storeDetail
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
fun loadProductDetail(productId: Int) {
viewModelScope.launch {
val result = repository.fetchProductDetail(productId)
_productDetail.value = result?.product
}
}
}
fun loadReviews(productId: Int) {
viewModelScope.launch {
val reviews = repository.fetchProductReview(productId)
_reviewProduct.value = reviews ?: emptyList()
}
}
}
// fun loadStoreDetail(storeId: Int){
// viewModelScope.launch {
// val storeResult = repository.fetchStoreDetail(storeId)
// _storeDetail.value = storeResult
// }
// }

View File

@ -0,0 +1,79 @@
package com.alya.ecommerce_serang.ui.product
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityReviewProductBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class ReviewProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewProductBinding
private lateinit var apiService: ApiService
private var reviewsAdapter: ReviewsAdapter? = null
private lateinit var sessionManager: SessionManager
private val viewModel: ProductViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReviewProductBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
val productId = intent.getIntExtra("PRODUCT_ID", -1) // Get the product ID
if (productId == -1) {
Log.e("ReviewProductActivity", "Invalid Product ID")
finish() // Close the activity if the product ID is invalid
return
}
setupRecyclerView()
viewModel.loadReviews(productId) // Fetch reviews using productId
observeReviews() // Observe review data
}
private fun observeReviews() {
viewModel.reviewProduct.observe(this) { reviews ->
if (reviews.isNotEmpty()) {
reviewsAdapter?.setReviews(reviews)
binding.tvNoReviews.visibility = View.GONE
} else {
binding.tvNoReviews.visibility = View.VISIBLE // Show "No Reviews" message
}
}
}
private fun setupRecyclerView() {
reviewsAdapter = ReviewsAdapter(
reviewList = emptyList()
)
binding.rvReviewsProduct.apply {
adapter = reviewsAdapter
layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
}
}
}

View File

@ -0,0 +1,61 @@
package com.alya.ecommerce_serang.ui.product
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class ReviewsAdapter(
private var reviewList: List<ReviewsItem>
) : RecyclerView.Adapter<ReviewsAdapter.ReviewViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReviewViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_review, parent, false)
return ReviewViewHolder(view)
}
override fun onBindViewHolder(holder: ReviewViewHolder, position: Int) {
val review = reviewList[position]
with(holder) {
tvReviewerName.text = review.username
tvReviewRating.text = review.rating.toString()
tvReviewText.text = review.reviewText
tvDateReview.text = formatDate(review.reviewDate)
}
}
override fun getItemCount(): Int = reviewList.size
fun setReviews(reviews: List<ReviewsItem>) {
reviewList = reviews
notifyDataSetChanged()
}
private fun formatDate(dateString: String): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) //from json
inputFormat.timeZone = TimeZone.getTimeZone("UTC") //get timezone
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) // new format
val date = inputFormat.parse(dateString) // Parse from json format
outputFormat.format(date!!) // convert to new format
} catch (e: Exception) {
dateString // Return original if error occurs
}
}
class ReviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvReviewRating: TextView = itemView.findViewById(R.id.tvReviewRating)
val tvReviewerName: TextView = itemView.findViewById(R.id.tvUsername)
val tvReviewText: TextView = itemView.findViewById(R.id.tvReviewText)
val tvDateReview: TextView = itemView.findViewById(R.id.date_review)
}
}

View File

@ -15,6 +15,7 @@
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">

View File

@ -0,0 +1,63 @@
<?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"
tools:context=".ui.product.ReviewProductActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:theme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageButton
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_back_24"
android:contentDescription="back"
android:background="?attr/selectableItemBackgroundBorderless"/>
<TextView
android:id="@+id/tv_review_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ulasan Produk"
android:textSize="20sp"
android:fontFamily="@font/dmsans_bold"
android:textStyle="bold"
android:gravity="center"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_reviews_product"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/topAppBar"
tools:itemCount="5"
tools:listitem="@layout/item_review" />
<TextView
android:id="@+id/tv_no_reviews"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Tidak Ada Ulasan"
app:layout_constraintTop_toBottomOf="@id/rv_reviews_product"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -50,6 +50,14 @@
android:textColor="@color/black" />
</LinearLayout>
<TextView
android:id="@+id/date_review"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_light"
android:textSize="12sp"
android:textColor="@color/soft_gray"
android:text="22-03-2025"/>
<!-- Review Text -->
<TextView
android:id="@+id/tvReviewText"