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 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
)

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.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>
}

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.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"

View File

@ -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
// }
}

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()
loadCategories()
}
}
sealed class HomeUiState {

View File

@ -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"/>

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: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"