mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
show detail product and category
This commit is contained in:
@ -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 {
|
||||
|
@ -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>
|
@ -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 price: Double,
|
||||
val rating: Double,
|
||||
val related: List<Product>,
|
||||
val related: List<ProductsItem>,
|
||||
val reviews: Int,
|
||||
val title: String,
|
||||
@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
|
||||
|
||||
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
|
||||
)
|
||||
|
@ -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.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>
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
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,
|
||||
|
@ -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
|
||||
}
|
||||
|
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_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"
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
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="forget_password">Lupa Kata Sandi?</string>
|
||||
<string name="enter_otp">Masukkan Kode OTP</string>
|
||||
<string name="sold_product_text">Produk Terlaris</string>
|
||||
</resources>
|
Reference in New Issue
Block a user