mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
add search
This commit is contained in:
@ -8,7 +8,11 @@ data class RegisterRequest (
|
||||
val password: String?,
|
||||
val username: String?,
|
||||
val phone: String?,
|
||||
@SerializedName("birth_date") val birthDate: String?,
|
||||
@SerializedName("userimg") val image: String?,
|
||||
@SerializedName("birth_date")
|
||||
val birthDate: String?,
|
||||
|
||||
@SerializedName("userimg")
|
||||
val image: String?,
|
||||
|
||||
val otp: String? = null
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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.OtpRequest
|
||||
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.response.product.CreateProductResponse
|
||||
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.OtpResponse
|
||||
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.product.AllProductResponse
|
||||
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.ProductResponse
|
||||
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.profile.AddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.HeaderMap
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
@ -202,4 +201,12 @@ interface ApiService {
|
||||
@Part("description") description: RequestBody,
|
||||
@Part complaintimg: MultipartBody.Part
|
||||
): Response<ComplaintResponse>
|
||||
|
||||
@POST("search")
|
||||
suspend fun saveSearchQuery(
|
||||
@Body searchRequest: SearchRequest
|
||||
): Response<CreateSearchResponse>
|
||||
|
||||
@GET("search")
|
||||
suspend fun getSearchHistory(): Response<SearchHistoryResponse>
|
||||
}
|
@ -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.CategoryItem
|
||||
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.product.CreateProductResponse
|
||||
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.Search
|
||||
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import java.io.File
|
||||
|
||||
class ProductRepository(private val apiService: ApiService) {
|
||||
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 {
|
||||
private const val TAG = "ProductRepository"
|
||||
|
@ -6,12 +6,14 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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.navigation.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
|
||||
@ -63,6 +65,8 @@ class HomeFragment : Fragment() {
|
||||
initUi()
|
||||
setupRecyclerView()
|
||||
observeData()
|
||||
setupSearchView()
|
||||
|
||||
}
|
||||
|
||||
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() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
@ -109,7 +147,7 @@ class HomeFragment : Fragment() {
|
||||
binding.loading.root.isVisible = false
|
||||
binding.error.root.isVisible = false
|
||||
binding.home.isVisible = true
|
||||
productAdapter?.updateLimitedProducts(state.products) // Ensure productAdapter is initialized
|
||||
productAdapter?.updateLimitedProducts(state.products)
|
||||
}
|
||||
is HomeUiState.Error -> {
|
||||
binding.loading.root.isVisible = false
|
||||
@ -125,18 +163,16 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
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()
|
||||
@ -161,7 +197,6 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun handleProductClick(product: ProductsItem) {
|
||||
val intent = Intent(requireContext(), DetailProductActivity::class.java)
|
||||
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
|
||||
@ -169,7 +204,7 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun handleCategoryProduct(category: CategoryItem) {
|
||||
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@ -179,7 +214,7 @@ class HomeFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun showLoading(isLoading: Boolean) {
|
||||
binding.progressBar.isVisible = isLoading
|
||||
}
|
||||
// private fun showLoading(isLoading: Boolean) {
|
||||
// binding.progressBar.isVisible = isLoading
|
||||
// }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,8 @@ class HomeViewModel (
|
||||
loadProducts()
|
||||
loadCategories()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
sealed class HomeUiState {
|
||||
|
@ -6,34 +6,28 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<include
|
||||
android:id="@+id/searchContainer"
|
||||
layout="@layout/view_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Home content in ScrollView -->
|
||||
<ScrollView
|
||||
android:id="@+id/home"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- Your existing home content here -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/searchContainer"
|
||||
layout="@layout/view_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
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_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
<!-- Remove searchContainer from here, it's now at the top level -->
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/banners"
|
||||
@ -41,7 +35,7 @@
|
||||
android:layout_height="132dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toBottomOf="@id/searchContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:layout_editor_absoluteX="16dp" />
|
||||
|
||||
<TextView
|
||||
@ -123,6 +117,58 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</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
|
||||
android:id="@+id/loading"
|
||||
layout="@layout/view_loading"/>
|
||||
|
76
app/src/main/res/layout/fragment_search_home.xml
Normal file
76
app/src/main/res/layout/fragment_search_home.xml
Normal 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>
|
67
app/src/main/res/layout/item_product_grid.xml
Normal file
67
app/src/main/res/layout/item_product_grid.xml
Normal 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>
|
32
app/src/main/res/layout/item_recent_search.xml
Normal file
32
app/src/main/res/layout/item_recent_search.xml
Normal 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>
|
@ -9,7 +9,11 @@
|
||||
android:id="@+id/homeFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.home.HomeFragment"
|
||||
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
|
||||
android:id="@+id/profileFragment"
|
||||
android:name="com.alya.ecommerce_serang.ui.profile.ProfileFragment"
|
||||
@ -20,6 +24,16 @@
|
||||
android:name="com.alya.ecommerce_serang.ui.chat.ChatFragment"
|
||||
android:label="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
|
||||
android:id="@+id/registerActivity"
|
||||
android:name="com.alya.ecommerce_serang.ui.auth.RegisterActivity"
|
||||
|
Reference in New Issue
Block a user