diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..dba9935 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,395 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a36f55..5be48a8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,10 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + id("kotlin-kapt") + id ("androidx.navigation.safeargs") + id("kotlin-parcelize") +// id("com.google.dagger.hilt.android") } android { @@ -9,7 +13,7 @@ android { defaultConfig { applicationId = "com.alya.ecommerce_serang" - minSdk = 24 + minSdk = 21 targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -19,21 +23,26 @@ android { buildTypes { release { + buildConfigField("String", "BASE_URL", "\"http://192.168.1.13:3000/\"") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } + debug { + buildConfigField("String", "BASE_URL", "\"http://192.168.1.13:3000/\"") + } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } buildFeatures { + buildConfig = true viewBinding = true } } @@ -45,7 +54,31 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.legacy.support.v4) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file + + //retrofit + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + + implementation("com.github.bumptech.glide:glide:4.16.0") + implementation("androidx.paging:paging-runtime:3.2.1") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + + +// implementation(libs.hilt.android) +// kapt("com.google.dagger:hilt-compiler:2.48") +// +// // For ViewModel injection (if needed) +// implementation(libs.androidx.hilt.lifecycle.viewmodel) +// kapt("androidx.hilt:hilt-compiler:1.0.0") +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 32b6e91..e00fc08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + tools:targetApi="31" + android:usesCleartextTraffic="true" + android:networkSecurityConfig="@xml/network_security_config"> diff --git a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt new file mode 100644 index 0000000..31a11eb --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt @@ -0,0 +1,7 @@ +package com.alya.ecommerce_serang.app + +import android.app.Application + +//@HiltAndroidApp +class App : Application(){ +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt new file mode 100644 index 0000000..95fe0bc --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Category.kt @@ -0,0 +1,11 @@ +package com.alya.ecommerce_serang.data.api.dto + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Category( + val id: String, + val image: String, + val title: String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt new file mode 100644 index 0000000..fe2355f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/DetailProduct.kt @@ -0,0 +1,22 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class DetailProduct( + val category: String, + val description: String, + val discount: Double?, + @SerializedName("favorite") + val wishlist: Boolean, + val id: String, + val images: List, + @SerializedName("in_stock") + val inStock: Int, + val price: Double, + val rating: Double, + val related: List, + val reviews: Int, + val title: String, + @SerializedName("free_delivery") + val freeDelivery:Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt new file mode 100644 index 0000000..4af2851 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Product.kt @@ -0,0 +1,17 @@ +package com.alya.ecommerce_serang.data.api.dto + + +import com.google.gson.annotations.SerializedName + +data class Product ( + val id: String, + val discount: Double?, + @SerializedName("favorite") + var wishlist: Boolean, + val image: String, + val price: Double, + val rating: Double, + @SerializedName("rating_count") + val ratingCount: Int, + val title: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt new file mode 100644 index 0000000..f4fd1a5 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/AllProductResponse.kt @@ -0,0 +1,57 @@ +package com.alya.ecommerce_serang.data.api.response + +import com.google.gson.annotations.SerializedName + +data class AllProductResponse( + + @field:SerializedName("message") + val message: String, + + @field:SerializedName("products") + val products: List +) + +data class ProductsItem( + + @field:SerializedName("store_id") + val storeId: Int, + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("rating") + val rating: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("weight") + val weight: Int, + + @field:SerializedName("is_pre_order") + val isPreOrder: Boolean, + + @field:SerializedName("category_id") + val categoryId: Int, + + @field:SerializedName("price") + val price: String, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("min_order") + val minOrder: Int, + + @field:SerializedName("total_sold") + val totalSold: Int, + + @field:SerializedName("stock") + val stock: Int, + + @field:SerializedName("status") + val status: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ProductResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ProductResponse.kt new file mode 100644 index 0000000..26b1130 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/ProductResponse.kt @@ -0,0 +1,60 @@ +package com.alya.ecommerce_serang.data.api.response + +import com.google.gson.annotations.SerializedName + +data class ProductResponse( + + @field:SerializedName("product") + val product: Product, + + @field:SerializedName("message") + val message: String +) + +data class Product( + + @field:SerializedName("store_id") + val storeId: Int, + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("rating") + val rating: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("weight") + val weight: Int, + + @field:SerializedName("product_name") + val productName: String, + + @field:SerializedName("is_pre_order") + val isPreOrder: Boolean, + + @field:SerializedName("duration") + val duration: Any, + + @field:SerializedName("category_id") + val categoryId: Int, + + @field:SerializedName("price") + val price: String, + + @field:SerializedName("product_id") + val productId: Int, + + @field:SerializedName("min_order") + val minOrder: Int, + + @field:SerializedName("total_sold") + val totalSold: Int, + + @field:SerializedName("stock") + val stock: Int, + + @field:SerializedName("product_category") + val productCategory: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt new file mode 100644 index 0000000..3a2e8f4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt @@ -0,0 +1,25 @@ +package com.alya.ecommerce_serang.data.api.retrofit + +import com.alya.ecommerce_serang.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class ApiConfig { + companion object{ + fun getApiService(): ApiService { + val loggingInterceptor = + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + val retrofit = Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + return retrofit.create(ApiService::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt new file mode 100644 index 0000000..711291d --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -0,0 +1,22 @@ +package com.alya.ecommerce_serang.data.api.retrofit + +import com.alya.ecommerce_serang.data.api.response.AllProductResponse +import com.alya.ecommerce_serang.data.api.response.ProductResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path + +interface ApiService { + @GET("product") + fun getAllProduct( + @Header("Authorization") token: String = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzEsIm5hbWUiOiJhbHlhIiwiZW1haWwiOiJha3VuYmVsYWphci5hbHlhQGdtYWlsLmNvbSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzM4NDg0OTc0LCJleHAiOjE3NDEwNzY5NzR9.0JyXJQ_6CKiZEi0gvk1gcn-0ILu3a9lOr3HqjhJXbBE" + ): Call + + + @GET("product/detail/{id}") + fun getDetailProduct ( + @Header("Authorization") token: String, + @Path("id") productId: Int + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/model/Product.kt b/app/src/main/java/com/alya/ecommerce_serang/data/model/Product.kt new file mode 100644 index 0000000..a5f79ea --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/model/Product.kt @@ -0,0 +1,51 @@ +package com.alya.ecommerce_serang.data.model + +import com.google.gson.annotations.SerializedName + +data class Product( + + @field:SerializedName("store_id") + val storeId: Int, + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("rating") + val rating: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("weight") + val weight: Int, + + @field:SerializedName("product_name") + val productName: String, + + @field:SerializedName("is_pre_order") + val isPreOrder: Boolean, + + @field:SerializedName("duration") + val duration: Any, + + @field:SerializedName("category_id") + val categoryId: Int, + + @field:SerializedName("price") + val price: String, + + @field:SerializedName("product_id") + val productId: Int, + + @field:SerializedName("min_order") + val minOrder: Int, + + @field:SerializedName("total_sold") + val totalSold: Int, + + @field:SerializedName("stock") + val stock: Int, + + @field:SerializedName("product_category") + val productCategory: String +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/model/User.kt b/app/src/main/java/com/alya/ecommerce_serang/data/model/User.kt new file mode 100644 index 0000000..c04a59b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/model/User.kt @@ -0,0 +1,9 @@ +package com.alya.ecommerce_serang.data.model + +data class User( + val username: String, + val avatar:String?, + val email:String, + val firstName:String?, + val lastName:String? +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt new file mode 100644 index 0000000..1f73cbd --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -0,0 +1,35 @@ +package com.alya.ecommerce_serang.data.repository + +import android.util.Log +import com.alya.ecommerce_serang.data.api.response.ProductsItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ProductRepository(private val apiService: ApiService) { + suspend fun getAllProducts(): Result> = + withContext(Dispatchers.IO) { + try { + Log.d("ProductRepository", "Attempting to fetch products") + val response = apiService.getAllProduct().execute() + Log.d("ProductRepository", "Response received. Success: ${response.isSuccessful}") + Log.d("ProductRepository", "Response code: ${response.code()}") + Log.d("ProductRepository", "Response message: ${response.message()}") + + if (response.isSuccessful) { + Result.success(response.body()?.products ?: emptyList()) + } else { + Result.failure(Exception("Failed to fetch products")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +// suspend fun getCategories():List +// +// fun getProducts(query: ProductQuery) : Flow> +// fun getRecentSearchs(): Flow> +// suspend fun clearRecents() +// suspend fun addRecents(search:String) +// suspend fun getProduct(id:String):DetailProduct +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt index 15e1e6e..9450c13 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt @@ -1,31 +1,57 @@ package com.alya.ecommerce_serang.ui import android.os.Bundle -import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.databinding.ActivityMainBinding -import com.google.android.material.bottomnavigation.BottomNavigationView - +//@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val navController by lazy { + (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - enableEdgeToEdge() - setContentView(R.layout.activity_main) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - val bottomNavigationView = findViewById(R.id.bottom_navigation) + setupBottomNavigation() + observeDestinationChanges() } + + private fun setupBottomNavigation() { + binding.bottomNavigation.setupWithNavController(navController) + + binding.bottomNavigation.setOnItemSelectedListener { menuItem -> + when (menuItem.itemId) { + R.id.home_item_fragment -> { + navController.navigate(R.id.homeFragment) + true + } + R.id.chat_item_fragment -> { + navController.navigate(R.id.chatFragment) + true + } + R.id.profile_item_fragment -> { + navController.navigate(R.id.profileFragment) + true + } + else -> false + } + } + } + + private fun observeDestinationChanges() { + navController.addOnDestinationChangedListener { _, destination, _ -> + binding.bottomNavigation.isVisible = when (destination.id) { + R.id.homeFragment, R.id.chatFragment, R.id.profileFragment -> true + else -> false // Bottom Navigation tidak terlihat di layar lain + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt new file mode 100644 index 0000000..614134d --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt @@ -0,0 +1,31 @@ +package com.alya.ecommerce_serang.ui.chat + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.alya.ecommerce_serang.R + +class ChatFragment : Fragment() { + + companion object { + fun newInstance() = ChatFragment() + } + + private val viewModel: ChatViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // TODO: Use the ViewModel + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_chat, container, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt new file mode 100644 index 0000000..01319f3 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -0,0 +1,7 @@ +package com.alya.ecommerce_serang.ui.chat + +import androidx.lifecycle.ViewModel + +class ChatViewModel : ViewModel() { + // TODO: Implement the ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt new file mode 100644 index 0000000..a2b2009 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeCategoryAdapter.kt @@ -0,0 +1,39 @@ +package com.alya.ecommerce_serang.ui.home + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.data.api.dto.Category +import com.alya.ecommerce_serang.databinding.ItemCategoryHomeBinding +import com.bumptech.glide.Glide + +class HomeCategoryAdapter( + private val categories:List, + //A lambda function that will be invoked when a category item is clicked. + private val onClick:(category:Category) -> Unit +): RecyclerView.Adapter() { + + /* + ViewHolder is responsible for caching and managing the view references for each item in + the RecyclerView.It binds the Category data to the corresponding views within the item layout. + */ + inner class ViewHolder(private val binding: ItemCategoryHomeBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(category: Category) = with(binding){ + Glide.with(root).load(category.image).into(image) + name.text = category.title + root.setOnClickListener{ + onClick(category) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder ( + ItemCategoryHomeBinding.inflate(LayoutInflater.from(parent.context),parent,false) + ) + + override fun getItemCount() = categories.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(categories[position]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt new file mode 100644 index 0000000..41e667e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -0,0 +1,145 @@ +package com.alya.ecommerce_serang.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.response.ProductsItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.ProductRepository +import com.alya.ecommerce_serang.databinding.FragmentHomeBinding +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration +import com.alya.ecommerce_serang.utils.setLightStatusBar +import kotlinx.coroutines.launch + +//@AndroidEntryPoint +class HomeFragment : Fragment() { + + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + private lateinit var viewModel: HomeViewModel + private var productAdapter: HorizontalProductAdapter? = null + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val repository = ProductRepository(ApiConfig.getApiService()) + viewModel = ViewModelProvider( + this, + // Pass a lambda that creates the ViewModel + BaseViewModelFactory { + HomeViewModel(repository) + } + )[HomeViewModel::class.java] + + initUi() + setupRecyclerView() + observeData() + } + + private fun setupRecyclerView() { + productAdapter = HorizontalProductAdapter( + products = emptyList(), + onClick = { product -> handleProductClick(product) } + ) + + binding.newProducts.apply { + adapter = productAdapter + layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + } + } + + private fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiState.collect { state -> + when (state) { + is HomeUiState.Loading -> { + binding.loading.root.isVisible = true + binding.error.root.isVisible = false + binding.home.isVisible = false + } + is HomeUiState.Success -> { + binding.loading.root.isVisible = false + binding.error.root.isVisible = false + binding.home.isVisible = true + productAdapter?.updateProducts(state.products) + } + is HomeUiState.Error -> { + binding.loading.root.isVisible = false + binding.error.root.isVisible = true + binding.home.isVisible = false + binding.error.errorMessage.text = state.message + binding.error.retryButton.setOnClickListener { + viewModel.retry() + } + } + } + } + } + } + + private fun initUi() { + // For LightStatusBar + setLightStatusBar() + val banners = binding.banners + banners.offscreenPageLimit = 1 + + val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible) + val currentItemHorizontalMarginPx = + resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin) + val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx + + banners.setPageTransformer { page, position -> + page.translationX = -pageTranslationX * position + page.scaleY = 1 - (0.25f * kotlin.math.abs(position)) + } + + banners.addItemDecoration( + HorizontalMarginItemDecoration( + requireContext(), + R.dimen.viewpager_current_item_horizontal_margin + ) + ) + } + + + private fun handleProductClick(product: ProductsItem) { + // Navigate to product detail +// findNavController().navigate( +// HomeFragmentDirections.actionHomeToDetail(product.id) +// ) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun showLoading(isLoading: Boolean) { + if (isLoading) { + binding.progressBar.visibility = View.VISIBLE + } else { + binding.progressBar.visibility = View.GONE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..71b52ce --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeViewModel.kt @@ -0,0 +1,60 @@ +package com.alya.ecommerce_serang.ui.home + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.response.AllProductResponse +import com.alya.ecommerce_serang.data.api.response.ProductsItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.ProductRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class HomeViewModel ( + private val productRepository: ProductRepository +): ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + val home = MutableLiveData(null) + constructor() : this(ProductRepository(ApiConfig.getApiService())) + + + init { + loadProducts() + } + + private fun loadProducts() { + viewModelScope.launch { + _uiState.value = HomeUiState.Loading + productRepository.getAllProducts() + .onSuccess { products -> + _uiState.value = HomeUiState.Success(products) + } + .onFailure { error -> + _uiState.value = HomeUiState.Error(error.message ?: "Unknown error") + Log.e("ProductViewModel", "Products fetch failed", error) + } + } + } + + fun retry() { + loadProducts() + } + +// fun toggleWishlist(product: Product) = viewModelScope.launch { +// try { +// productRepository.toggleWishlist(product.id,product.wishlist) +// }catch (e:Exception){ +// +// } +// } +} + +sealed class HomeUiState { + object Loading : HomeUiState() + data class Success(val products: List) : HomeUiState() + data class Error(val message: String) : HomeUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt new file mode 100644 index 0000000..61c48b8 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HorizontalProductAdapter.kt @@ -0,0 +1,52 @@ +package com.alya.ecommerce_serang.ui.home + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.data.api.response.ProductsItem +import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding +import com.bumptech.glide.Glide + +class HorizontalProductAdapter( + private var products: List, + private val onClick: (ProductsItem) -> Unit +) : RecyclerView.Adapter() { + + inner class ProductViewHolder(private val binding: ItemProductHorizontalBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(product: ProductsItem) = with(binding) { + itemName.text = product.name + itemPrice.text = product.price + rating.text = product.rating +// productSold.text = "${product.totalSold} sold" + + // Load image using Glide + Glide.with(itemView) +// .load("${BuildConfig.BASE_URL}/product/${product.image}") +// .load("${BuildConfig.BASE_URL}/${product.image}") + .load(product.image) + .into(image) + + root.setOnClickListener { onClick(product) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { + val binding = ItemProductHorizontalBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ProductViewHolder(binding) + } + + override fun getItemCount() = products.size + + override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { + holder.bind(products[position]) + } + + fun updateProducts(newProducts: List) { + products = newProducts + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt new file mode 100644 index 0000000..43a7e0e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/ProductViewHolder.kt @@ -0,0 +1,35 @@ +package com.alya.ecommerce_serang.ui.product + +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.Product +import com.alya.ecommerce_serang.databinding.ItemProductHorizontalBinding +import com.bumptech.glide.Glide + +class ProductViewHolder(private val binding: ItemProductHorizontalBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + product: Product, + onClick: (product: Product) -> Unit, + ) = with(binding) { + Glide.with(root).load(product.image).into(image) + +// discount.isVisible = product.discount != null +// product.discount?.let { +// val discount = (product.discount / product.price * 100).roundToInt() +// binding.discount.text = +// root.context.getString(R.string.fragment_item_product_discount, discount) +// } + + itemName.text = product.title + rating.text = String.format("%.1f", product.rating) + +// val current = product.price - (product.discount ?: 0.0) + val current = product.price + itemPrice.text = root.context.getString(R.string.item_price_txt, current) + + root.setOnClickListener { + onClick(product) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt new file mode 100644 index 0000000..2c7a149 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt @@ -0,0 +1,31 @@ +package com.alya.ecommerce_serang.ui.profile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.alya.ecommerce_serang.R + +class ProfileFragment : Fragment() { + + companion object { + fun newInstance() = ProfileFragment() + } + + private val viewModel: ProfileViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // TODO: Use the ViewModel + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_profile, container, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileViewModel.kt new file mode 100644 index 0000000..fb0e34f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileViewModel.kt @@ -0,0 +1,7 @@ +package com.alya.ecommerce_serang.ui.profile + +import androidx.lifecycle.ViewModel + +class ProfileViewModel : ViewModel() { + // TODO: Implement the ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/BaseFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/BaseFragment.kt new file mode 100644 index 0000000..19b9ea1 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/BaseFragment.kt @@ -0,0 +1,7 @@ +package com.alya.ecommerce_serang.utils + +import android.view.LayoutInflater +import android.view.ViewGroup + +typealias Inflate = (LayoutInflater, ViewGroup?, Boolean) -> T + diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/BaseViewModelFactory.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/BaseViewModelFactory.kt new file mode 100644 index 0000000..9673190 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/BaseViewModelFactory.kt @@ -0,0 +1,13 @@ +package com.alya.ecommerce_serang.utils + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class BaseViewModelFactory( + private val creator: () -> VM +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return creator() as T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/Functions.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/Functions.kt new file mode 100644 index 0000000..9e1ba9c --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/Functions.kt @@ -0,0 +1,22 @@ +package com.alya.ecommerce_serang.utils + +import android.os.Build +import android.view.View +import android.view.WindowInsetsController +import androidx.fragment.app.Fragment + +fun Fragment.setLightStatusBar(){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requireActivity().window.insetsController?.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS) + } else { + @Suppress("DEPRECATION") + requireActivity().window.decorView.systemUiVisibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/HorizontalMarginItemDecoration.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/HorizontalMarginItemDecoration.kt new file mode 100644 index 0000000..28d9a19 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/HorizontalMarginItemDecoration.kt @@ -0,0 +1,22 @@ +package com.alya.ecommerce_serang.utils + +import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.annotation.DimenRes +import androidx.recyclerview.widget.RecyclerView + +class HorizontalMarginItemDecoration(context: Context, @DimenRes horizontalMarginInDp: Int) : + RecyclerView.ItemDecoration() { + + private val horizontalMarginInPx: Int = + context.resources.getDimension(horizontalMarginInDp).toInt() + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State + ) { + outRect.right = horizontalMarginInPx + outRect.left = horizontalMarginInPx + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt new file mode 100644 index 0000000..83f557e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/ProductQuery.kt @@ -0,0 +1,20 @@ +package com.alya.ecommerce_serang.utils + +import android.os.Parcelable +import com.alya.ecommerce_serang.data.api.dto.Category +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ProductQuery ( + val category: Category? = null, + val search:String? = null, + val range:Pair = 0f to 10000f, + val rating:Int? = null, + val discount:Int? = null, + val sort:List = emptyList(), + val favorite:Boolean = false +): Parcelable + +enum class Sort{ + disconunt, voucher, shipping, delivery +} \ No newline at end of file diff --git a/app/src/main/res/drawable/banner_default.jpg b/app/src/main/res/drawable/banner_default.jpg new file mode 100644 index 0000000..4b227f5 Binary files /dev/null and b/app/src/main/res/drawable/banner_default.jpg differ diff --git a/app/src/main/res/drawable/baseline_account_circle_24.xml b/app/src/main/res/drawable/baseline_account_circle_24.xml new file mode 100644 index 0000000..da4b6e5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_account_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_arrow_back_24.xml b/app/src/main/res/drawable/baseline_arrow_back_24.xml new file mode 100644 index 0000000..f9c67c9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_back_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_chat_24.xml b/app/src/main/res/drawable/baseline_chat_24.xml new file mode 100644 index 0000000..7fc5c09 --- /dev/null +++ b/app/src/main/res/drawable/baseline_chat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000..b464bbb --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_search_24.xml b/app/src/main/res/drawable/baseline_search_24.xml new file mode 100644 index 0000000..27a43c1 --- /dev/null +++ b/app/src/main/res/drawable/baseline_search_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_star_24.xml b/app/src/main/res/drawable/baseline_star_24.xml new file mode 100644 index 0000000..cbd9a83 --- /dev/null +++ b/app/src/main/res/drawable/baseline_star_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/makanan_ringan.jpg b/app/src/main/res/drawable/makanan_ringan.jpg new file mode 100644 index 0000000..1ff7d41 Binary files /dev/null and b/app/src/main/res/drawable/makanan_ringan.jpg differ diff --git a/app/src/main/res/drawable/outline_notifications_24.xml b/app/src/main/res/drawable/outline_notifications_24.xml new file mode 100644 index 0000000..aeb6101 --- /dev/null +++ b/app/src/main/res/drawable/outline_notifications_24.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_shopping_cart_24.xml b/app/src/main/res/drawable/outline_shopping_cart_24.xml new file mode 100644 index 0000000..8680c2d --- /dev/null +++ b/app/src/main/res/drawable/outline_shopping_cart_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rating_background.xml b/app/src/main/res/drawable/rating_background.xml new file mode 100644 index 0000000..4dfd157 --- /dev/null +++ b/app/src/main/res/drawable/rating_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_background.xml b/app/src/main/res/drawable/search_background.xml new file mode 100644 index 0000000..86c8a8f --- /dev/null +++ b/app/src/main/res/drawable/search_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_account.xml b/app/src/main/res/drawable/selector_account.xml new file mode 100644 index 0000000..e20e513 --- /dev/null +++ b/app/src/main/res/drawable/selector_account.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_chat.xml b/app/src/main/res/drawable/selector_chat.xml new file mode 100644 index 0000000..0675b22 --- /dev/null +++ b/app/src/main/res/drawable/selector_chat.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_home.xml b/app/src/main/res/drawable/selector_home.xml new file mode 100644 index 0000000..67b8c01 --- /dev/null +++ b/app/src/main/res/drawable/selector_home.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a953d86..0e10077 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - + + - - + + + app:itemIconSize="@dimen/m3_comp_navigation_bar_active_indicator_height" /> - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml new file mode 100644 index 0000000..4f1189a --- /dev/null +++ b/app/src/main/res/layout/fragment_chat.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..7cac89a --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..28e1eec --- /dev/null +++ b/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml new file mode 100644 index 0000000..77d9ef6 --- /dev/null +++ b/app/src/main/res/layout/item_category.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category_home.xml b/app/src/main/res/layout/item_category_home.xml new file mode 100644 index 0000000..3a6604a --- /dev/null +++ b/app/src/main/res/layout/item_category_home.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_product_horizontal.xml b/app/src/main/res/layout/item_product_horizontal.xml new file mode 100644 index 0000000..bfae57b --- /dev/null +++ b/app/src/main/res/layout/item_product_horizontal.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_section_horizontal.xml b/app/src/main/res/layout/item_section_horizontal.xml new file mode 100644 index 0000000..5b8d4ed --- /dev/null +++ b/app/src/main/res/layout/item_section_horizontal.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_section_vertical.xml b/app/src/main/res/layout/item_section_vertical.xml new file mode 100644 index 0000000..711855e --- /dev/null +++ b/app/src/main/res/layout/item_section_vertical.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_error.xml b/app/src/main/res/layout/view_error.xml new file mode 100644 index 0000000..971a5e9 --- /dev/null +++ b/app/src/main/res/layout/view_error.xml @@ -0,0 +1,31 @@ + + + + + +