add search

This commit is contained in:
shaulascr
2025-04-21 05:28:26 +07:00
parent 78b75651ff
commit 4336142e1a
17 changed files with 809 additions and 43 deletions

View File

@ -8,7 +8,11 @@ data class RegisterRequest (
val password: String?, val password: String?,
val username: String?, val username: String?,
val phone: String?, val phone: String?,
@SerializedName("birth_date") val birthDate: String?, @SerializedName("birth_date")
@SerializedName("userimg") val image: String?, val birthDate: String?,
@SerializedName("userimg")
val image: String?,
val otp: String? = null val otp: String? = null
) )

View File

@ -0,0 +1,8 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class SearchRequest(
@SerializedName("search_query")
val searchQuery: String
)

View File

@ -0,0 +1,24 @@
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class CreateSearchResponse(
@field:SerializedName("search")
val search: Search
)
data class Search(
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("search_query")
val searchQuery: String
)

View File

@ -0,0 +1,15 @@
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class SearchHistoryResponse(
@field:SerializedName("data")
val data: List<DataItem>
)
data class DataItem(
@field:SerializedName("search_query")
val searchQuery: String
)

View File

@ -10,11 +10,9 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
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.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
@ -32,21 +30,22 @@ import com.alya.ecommerce_serang.data.api.response.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderListResponse import com.alya.ecommerce_serang.data.api.response.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.product.AllProductResponse import com.alya.ecommerce_serang.data.api.response.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.product.CategoryResponse import com.alya.ecommerce_serang.data.api.response.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.product.DetailStoreProductResponse import com.alya.ecommerce_serang.data.api.response.product.DetailStoreProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ReviewProductResponse import com.alya.ecommerce_serang.data.api.response.product.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.HeaderMap
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
@ -202,4 +201,12 @@ interface ApiService {
@Part("description") description: RequestBody, @Part("description") description: RequestBody,
@Part complaintimg: MultipartBody.Part @Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse> ): Response<ComplaintResponse>
@POST("search")
suspend fun saveSearchQuery(
@Body searchRequest: SearchRequest
): Response<CreateSearchResponse>
@GET("search")
suspend fun getSearchHistory(): Response<SearchHistoryResponse>
} }

View File

@ -4,19 +4,19 @@ import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CategoryItem 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.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.Search
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import java.io.File
class ProductRepository(private val apiService: ApiService) { class ProductRepository(private val apiService: ApiService) {
suspend fun getAllProducts(): Result<List<ProductsItem>> = suspend fun getAllProducts(): Result<List<ProductsItem>> =
@ -192,6 +192,70 @@ class ProductRepository(private val apiService: ApiService) {
} }
} }
suspend fun searchProducts(query: String): Result<List<ProductsItem>> =
withContext(Dispatchers.IO) {
try {
// First save the search query
saveSearchQuery(query)
// Then fetch all products
val response = apiService.getAllProduct()
if (response.isSuccessful) {
val allProducts = response.body()?.products ?: emptyList()
// Filter products based on the search query
val filteredProducts = allProducts.filter { product ->
product.name.contains(query, ignoreCase = true) ||
(product.description?.contains(query, ignoreCase = true) ?: false)
}
Log.d(TAG, "Found ${filteredProducts.size} products matching '$query'")
Result.Success(filteredProducts)
} else {
Result.Error(Exception("Failed to fetch products for search. Code: ${response.code()}"))
}
} catch (e: Exception) {
Log.e(TAG, "Error searching products", e)
Result.Error(e)
}
}
suspend fun saveSearchQuery(query: String): Result<Search?> =
withContext(Dispatchers.IO) {
try {
val response = apiService.saveSearchQuery(SearchRequest(query))
if (response.isSuccessful) {
Result.Success(response.body()?.search)
} else {
Log.e(TAG, "Failed to save search query. Code: ${response.code()}")
Result.Error(Exception("Failed to save search query"))
}
} catch (e: Exception) {
Log.e(TAG, "Error saving search query", e)
Result.Error(e)
}
}
suspend fun getSearchHistory(): Result<List<String>> =
withContext(Dispatchers.IO) {
try {
val response = apiService.getSearchHistory()
if (response.isSuccessful) {
val searches = response.body()?.data?.map { it.searchQuery } ?: emptyList()
Result.Success(searches)
} else {
Log.e(TAG, "Failed to fetch search history. Code: ${response.code()}")
Result.Error(Exception("Failed to fetch search history"))
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching search history", e)
Result.Error(e)
}
}
companion object { companion object {
private const val TAG = "ProductRepository" private const val TAG = "ProductRepository"

View File

@ -6,12 +6,14 @@ 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 android.view.inputmethod.EditorInfo
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.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
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.dto.CategoryItem import com.alya.ecommerce_serang.data.api.dto.CategoryItem
@ -63,6 +65,8 @@ class HomeFragment : Fragment() {
initUi() initUi()
setupRecyclerView() setupRecyclerView()
observeData() observeData()
setupSearchView()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -95,6 +99,40 @@ class HomeFragment : Fragment() {
} }
} }
private fun setupSearchView() {
binding.searchContainer.search.apply {
// When user clicks the search box, navigate to search fragment
setOnClickListener {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(null)
)
}
// Handle search action if user presses search on keyboard
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val query = text.toString().trim()
if (query.isNotEmpty()) {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(query)
)
}
return@setOnEditorActionListener true
}
false
}
}
// Setup cart and notification buttons
binding.searchContainer.btnCart.setOnClickListener {
// Navigate to cart
}
binding.searchContainer.btnNotification.setOnClickListener {
// Navigate to notifications
}
}
private fun observeData() { private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -109,7 +147,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?.updateLimitedProducts(state.products) // Ensure productAdapter is initialized productAdapter?.updateLimitedProducts(state.products)
} }
is HomeUiState.Error -> { is HomeUiState.Error -> {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
@ -129,14 +167,12 @@ class HomeFragment : Fragment() {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.categories.collect { categories -> viewModel.categories.collect { categories ->
Log.d("Categories", "Updated Categories: $categories") Log.d("Categories", "Updated Categories: $categories")
categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") }
categoryAdapter?.updateLimitedCategory(categories) categoryAdapter?.updateLimitedCategory(categories)
} }
} }
} }
} }
private fun initUi() { private fun initUi() {
// For LightStatusBar // For LightStatusBar
setLightStatusBar() setLightStatusBar()
@ -161,7 +197,6 @@ class HomeFragment : Fragment() {
) )
} }
private fun handleProductClick(product: ProductsItem) { private fun handleProductClick(product: ProductsItem) {
val intent = Intent(requireContext(), DetailProductActivity::class.java) val intent = Intent(requireContext(), DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
@ -169,7 +204,7 @@ class HomeFragment : Fragment() {
} }
private fun handleCategoryProduct(category: CategoryItem) { private fun handleCategoryProduct(category: CategoryItem) {
// Your implementation
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -179,7 +214,7 @@ class HomeFragment : Fragment() {
_binding = null _binding = null
} }
private fun showLoading(isLoading: Boolean) { // private fun showLoading(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading // binding.progressBar.isVisible = isLoading
} // }
} }

View File

@ -0,0 +1,56 @@
package com.alya.ecommerce_serang.ui.home
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.databinding.ItemRecentSearchBinding
class SearchHistoryAdapter(
private val onItemClick: (String) -> Unit
) : ListAdapter<String, SearchHistoryAdapter.ViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemRecentSearchBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val query = getItem(position)
holder.bind(query)
}
inner class ViewHolder(private val binding: ItemRecentSearchBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
onItemClick(getItem(position))
}
}
}
fun bind(query: String) {
binding.recentSearchText.text = query
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}
}

View File

@ -0,0 +1,162 @@
package com.alya.ecommerce_serang.ui.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
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.FragmentSearchHomeBinding
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class SearchHomeFragment : Fragment() {
private var _binding: FragmentSearchHomeBinding? = null
private val binding get() = _binding!!
private var searchResultsAdapter: SearchResultsAdapter? = null
private lateinit var sessionManager: SessionManager
private val args: SearchHomeFragmentArgs by navArgs()
private val viewModel: SearchHomeViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
SearchHomeViewModel(productRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSearchHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUI()
setupSearchResultsRecyclerView()
observeData()
// Perform search with the query passed from HomeFragment
args.query?.let { query ->
// Wait until layout is done, then set query text
binding.searchView.post {
binding.searchView.setQuery(query, false) // sets "food" as text, doesn't submit
}
viewModel.searchProducts(query)
}
}
private fun setupUI() {
// Setup back button
binding.backButton.setOnClickListener {
findNavController().navigateUp()
}
// Setup search view
binding.searchView.apply {
setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
query?.let {
if (it.isNotEmpty()) {
viewModel.searchProducts(it)
hideKeyboard()
}
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
newText?.let {
if (it.isEmpty()) {
// Clear the search results if user clears the input
searchResultsAdapter?.submitList(emptyList())
binding.noResultsText.isVisible = false
return true
}
// Optional: do real-time search
if (it.length >= 2) {
viewModel.searchProducts(it)
}
}
return true
}
})
// Request focus and show keyboard
if (args.query.isNullOrEmpty()) {
requestFocus()
postDelayed({
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(findFocus(), InputMethodManager.SHOW_IMPLICIT)
}, 200)
}
}
}
private fun setupSearchResultsRecyclerView() {
searchResultsAdapter = SearchResultsAdapter { product ->
navigateToProductDetail(product)
}
binding.searchResultsRecyclerView.apply {
adapter = searchResultsAdapter
layoutManager = GridLayoutManager(requireContext(), 2)
}
}
private fun observeData() {
viewModel.searchResults.observe(viewLifecycleOwner) { products ->
searchResultsAdapter?.submitList(products)
binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!!
binding.searchResultsRecyclerView.isVisible = products.isNotEmpty()
}
viewModel.isSearching.observe(viewLifecycleOwner) { isSearching ->
binding.progressBar.isVisible = isSearching
}
}
private fun navigateToProductDetail(product: ProductsItem) {
val intent = Intent(requireContext(), DetailProductActivity::class.java)
intent.putExtra("PRODUCT_ID", product.id)
startActivity(intent)
}
private fun hideKeyboard() {
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
binding.searchView.let {
imm.hideSoftInputFromWindow(it.windowToken, 0)
}
}
override fun onDestroyView() {
super.onDestroyView()
searchResultsAdapter = null
_binding = null
}
}

View File

@ -0,0 +1,76 @@
package com.alya.ecommerce_serang.ui.home
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.launch
class SearchHomeViewModel (private val productRepository: ProductRepository) : ViewModel() {
private val _searchResults = MutableLiveData<List<ProductsItem>>(emptyList())
val searchResults: LiveData<List<ProductsItem>> = _searchResults
private val _searchHistory = MutableLiveData<List<String>>(emptyList())
val searchHistory: LiveData<List<String>> = _searchHistory
private val _isSearching = MutableLiveData(false)
val isSearching: LiveData<Boolean> = _isSearching
private val _isSearchActive = MutableLiveData(false)
val isSearchActive: LiveData<Boolean> = _isSearchActive
fun searchProducts(query: String) {
Log.d("HomeViewModel", "searchProducts called with query: '$query'")
if (query.isBlank()) {
Log.d("HomeViewModel", "Query is blank, clearing results")
_searchResults.value = emptyList()
_isSearchActive.value = false
return
}
_isSearching.value = true
_isSearchActive.value = true
viewModelScope.launch {
Log.d("HomeViewModel", "Starting search coroutine")
when (val result = productRepository.searchProducts(query)) {
is Result.Success -> {
Log.d("HomeViewModel", "Search successful, found ${result.data.size} products")
_searchResults.postValue(result.data)
// Double check the state after assignment
Log.d("HomeViewModel", "Updated searchResults value has ${result.data.size} items")
}
is Result.Error -> {
Log.e("HomeViewModel", "Search failed", result.exception)
_searchResults.postValue(emptyList())
}
else -> {}
}
_isSearching.postValue(false)
}
}
fun clearSearch() {
_isSearchActive.value = false
_searchResults.value = emptyList()
_isSearching.value = false
}
fun loadSearchHistory() {
viewModelScope.launch {
when (val result = productRepository.getSearchHistory()) {
is Result.Success -> _searchHistory.value = result.data
is Result.Error -> Log.e("HomeViewModel", "Failed to load search history", result.exception)
else -> {}
}
}
}
}

View File

@ -0,0 +1,78 @@
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.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
import com.bumptech.glide.Glide
class SearchResultsAdapter(
private val onItemClick: (ProductsItem) -> Unit
) : ListAdapter<ProductsItem, SearchResultsAdapter.ViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemProductGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = getItem(position)
holder.bind(product)
}
inner class ViewHolder(private val binding: ItemProductGridBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
onItemClick(getItem(position))
}
}
}
fun bind(product: ProductsItem) {
binding.productName.text = product.name
binding.productPrice.text = (product.price)
// Load image with Glide
Glide.with(binding.root.context)
.load(product.image)
.placeholder(R.drawable.placeholder_image)
// .error(R.drawable.error_image)
.into(binding.productImage)
// Set store name if available
product.storeId?.toString().let {
binding.storeName.text = it
}
}
}
override fun submitList(list: List<ProductsItem>?) {
Log.d("SearchResultsAdapter", "Submitting list with ${list?.size ?: 0} items")
super.submitList(list)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ProductsItem>() {
override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean {
return oldItem == newItem
}
}
}
}

View File

@ -52,6 +52,8 @@ class HomeViewModel (
loadProducts() loadProducts()
loadCategories() loadCategories()
} }
} }
sealed class HomeUiState { sealed class HomeUiState {

View File

@ -6,15 +6,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.home.HomeFragment"> tools:context=".ui.home.HomeFragment">
<ScrollView
android:id="@+id/home"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include <include
android:id="@+id/searchContainer" android:id="@+id/searchContainer"
layout="@layout/view_search" layout="@layout/view_search"
@ -23,17 +14,20 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ProgressBar <!-- Home content in ScrollView -->
android:id="@+id/progressBar" <ScrollView
style="?android:attr/progressBarStyle" android:id="@+id/home"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/searchContainer"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" <!-- Your existing home content here -->
app:layout_constraintTop_toTopOf="parent" <androidx.constraintlayout.widget.ConstraintLayout
tools:visibility="visible" /> android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Remove searchContainer from here, it's now at the top level -->
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/banners" android:id="@+id/banners"
@ -41,7 +35,7 @@
android:layout_height="132dp" android:layout_height="132dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/searchContainer" app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="16dp" /> tools:layout_editor_absoluteX="16dp" />
<TextView <TextView
@ -123,6 +117,58 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultsRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/searchContainer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- All other search-related elements at the top level -->
<LinearLayout
android:id="@+id/searchHistoryHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/searchContainer">
<!-- ... -->
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchHistoryRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/searchHistoryHeader"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/noResultsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No results found"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/searchContainer" />
<ProgressBar
android:id="@+id/searchProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/searchContainer" />
<include <include
android:id="@+id/loading" android:id="@+id/loading"
layout="@layout/view_loading"/> layout="@layout/view_loading"/>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.home.SearchHomeFragment">
<LinearLayout
android:id="@+id/searchToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/backButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Kembali"
android:src="@drawable/ic_back_24" />
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="@drawable/search_background"
android:iconifiedByDefault="false"
android:queryHint="Search products..." />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/light_gray"
app:layout_constraintTop_toBottomOf="@id/searchToolbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultsRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
app:spanCount="2"
tools:listitem="@layout/item_product_grid" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />
<TextView
android:id="@+id/noResultsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No results found"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/productImage"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/placeholder_image" />
<TextView
android:id="@+id/productName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/productImage"
tools:text="Product Name" />
<TextView
android:id="@+id/storeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="12sp"
android:fontFamily="@font/dmsans_medium"
app:layout_constraintTop_toBottomOf="@id/productName"
tools:text="Store Name" />
<TextView
android:id="@+id/productPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textColor="@color/blue1"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/storeName"
tools:text="Rp 150.000" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,32 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="16dp">
<ImageView
android:id="@+id/historyIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/outline_home_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/recentSearchText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
android:text="Cari Produk"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/historyIcon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,7 +9,11 @@
android:id="@+id/homeFragment" android:id="@+id/homeFragment"
android:name="com.alya.ecommerce_serang.ui.home.HomeFragment" android:name="com.alya.ecommerce_serang.ui.home.HomeFragment"
android:label="fragment_home" android:label="fragment_home"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_homeFragment_to_searchHomeFragment"
app:destination="@id/searchHomeFragment" />
</fragment>
<fragment <fragment
android:id="@+id/profileFragment" android:id="@+id/profileFragment"
android:name="com.alya.ecommerce_serang.ui.profile.ProfileFragment" android:name="com.alya.ecommerce_serang.ui.profile.ProfileFragment"
@ -20,6 +24,16 @@
android:name="com.alya.ecommerce_serang.ui.chat.ChatFragment" android:name="com.alya.ecommerce_serang.ui.chat.ChatFragment"
android:label="fragment_chat" android:label="fragment_chat"
tools:layout="@layout/fragment_chat" /> tools:layout="@layout/fragment_chat" />
<fragment
android:id="@+id/searchHomeFragment"
android:name="com.alya.ecommerce_serang.ui.home.SearchHomeFragment"
android:label="Search"
tools:layout="@layout/fragment_search_home">
<argument
android:name="query"
app:argType="string"
app:nullable="true" />
</fragment>
<activity <activity
android:id="@+id/registerActivity" android:id="@+id/registerActivity"
android:name="com.alya.ecommerce_serang.ui.auth.RegisterActivity" android:name="com.alya.ecommerce_serang.ui.auth.RegisterActivity"