feat: implement commodity price prediction feature

This commit introduces the commodity price prediction feature, including:

- **Data Layer:**
    - Models for commodity prediction and price responses.
    - `CommodityApiService` for fetching data from the backend.
    - `CommodityRepository` to abstract data fetching logic.
- **DI Modules:**
    - `appModule` for providing `CommodityRepository`.
    - `networkModule` for setting up Retrofit and OkHttp.
    - `viewModelModule` for providing `PredictionViewModel` and `CommodityViewModel`.
- **Presentation Layer:**
    - `PredictionViewModel` and `CommodityViewModel` to handle business logic and expose data to UI.
    - `PredictionScreen` to display commodity price predictions with a dropdown to select commodities and detailed breakdown of future prices (tomorrow, next week, next month, next 3 months).
    - `CommodityPriceScreen` (initial setup, details to be implemented) to display current commodity prices.
    - `CommodityListItem` composable for displaying individual commodity price information.
    - `FeatureBanner` composable for both commodity price prediction and control features.
- **Navigation:**
    - `SetupNavigation` to define navigation routes for `CommodityPriceScreen` and `PredictionScreen`.
- **Core Utilities:**
    - `toRupiahFormat()` extension function to format numbers as Indonesian Rupiah.
    - `percentageChange()` function to calculate percentage difference.
This commit is contained in:
Cutiful 2025-07-30 13:55:25 +07:00
parent 30c751be7a
commit a822c1782e
16 changed files with 744 additions and 0 deletions

View File

@ -0,0 +1,54 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.R
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun FeatureBanner(){
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = MainGreen, shape = RoundedCornerShape(8.dp))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.fillMaxWidth(0.6f)) {
Text(
"Prediksi Harga",
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
)
Text(
"Agar kamu bisa memilih komoditas yang tepat",
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
)
}
Image(
modifier = Modifier.fillMaxWidth(0.8f),
painter = painterResource(id = R.drawable.commodity_img_banner),
contentDescription = "Plant Image",
)
}
}
}

View File

@ -0,0 +1,16 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils
import java.text.NumberFormat
import java.util.Locale
fun Number.toRupiahFormat(): String {
val localeID = Locale("in", "ID")
val numberFormat = NumberFormat.getCurrencyInstance(localeID)
return numberFormat.format(this.toLong()).replace(",00", "")
}
fun percentageChange(from: Number, to: Number): Double {
if (from.toDouble() == 0.0 || to.toDouble() == 0.0) return 0.0
val change = ((to.toDouble() - from.toDouble()) / from.toDouble()) * 100
return change
}

View File

@ -0,0 +1,32 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model
data class CommodityPredictionResponse(
val harga: Double? = null,
val nama_komoditas: String,
val tanggal: String? = null,
val val_1: Double? = null,
val val_7: Double? = null,
val val_30: Double? = null,
val val_90: Double? = null
)
//class CommodityPredictionDeserializer : JsonDeserializer<CommodityPredictionResponse> {
// override fun deserialize(
// json: JsonElement,
// typeOfT: Type,
// context: JsonDeserializationContext
// ): CommodityPredictionResponse {
// val jsonObject = json.asJsonObject
// val name = jsonObject.get("nama_komoditas").asString
// val date = jsonObject.get("tanggal").asString
//
// val predictions = mutableMapOf<String, Double>()
// jsonObject.entrySet().forEach { entry ->
// if (entry.key.startsWith("val_")) {
// predictions[entry.key] = entry.value.asDouble
// }
// }
//
// return CommodityPredictionResponse(name, date, predictions)
// }
//}

View File

@ -0,0 +1,10 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model
data class CommodityPriceResponseItem(
val harga: Int?,
val kab_kota: String?,
val komoditas_nama: String?,
val pasar: String?,
val satuan: String?,
val tanggal: String?
)

View File

@ -0,0 +1,22 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
import retrofit2.http.GET
import retrofit2.http.Query
interface CommodityApiService {
@GET("api/periodic-data")
suspend fun getCommodityPredictions(
@Query("tanggal") date: String,
): List<CommodityPredictionResponse>
@GET("api/harga")
suspend fun getCommodityPrice(
@Query("komoditas") komoditas: String,
@Query("pasar") pasar: String,
@Query("tanggal") tanggal: String,
@Query("kab_kota") kabKota: String
): List<CommodityPriceResponseItem>
}

View File

@ -0,0 +1,15 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network.CommodityApiService
class CommodityRepository(private val apiService: CommodityApiService) {
suspend fun fetchCommodityPredictions(date: String): List<CommodityPredictionResponse> {
return apiService.getCommodityPredictions(date)
}
suspend fun fetchCommodityPrice(commodityName: String, marketName: String, date: String, city: String): List<CommodityPriceResponseItem> {
return apiService.getCommodityPrice(komoditas = commodityName, pasar = marketName, tanggal = date, kabKota = city)
}
}

View File

@ -0,0 +1,8 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
import org.koin.dsl.module
val appModule = module {
single { CommodityRepository(get()) }
}

View File

@ -0,0 +1,26 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network.CommodityApiService
import okhttp3.OkHttpClient
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.build()
}
single {
Retrofit.Builder()
.baseUrl("http://labai.polinema.ac.id:50/")
.client(get())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single { get<Retrofit>().create(CommodityApiService::class.java) }
}

View File

@ -0,0 +1,11 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.CommodityViewModel
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PredictionViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
viewModel { PredictionViewModel(get()) }
viewModel { CommodityViewModel(get()) }
}

View File

@ -0,0 +1,20 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.CommodityPriceScreen
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PredictionScreen
@Composable
fun SetupNavigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = "commodity_screen") {
composable("commodity_screen") {
CommodityPriceScreen(navController = navController)
}
composable("prediction_screen") {
PredictionScreen(navController = navController)
}
}
}

View File

@ -0,0 +1,51 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private const val TAG = "CommodityViewModel"
class CommodityViewModel(
private val repository: CommodityRepository
) : ViewModel() {
private val _state = MutableStateFlow<ResultState<List<CommodityPriceResponseItem>>>(
ResultState.Idle
)
val state: StateFlow<ResultState<List<CommodityPriceResponseItem>>> = _state
fun loadCommodityPrice(
commodityName: String,
market: String,
date: String,
city: String,
) {
viewModelScope.launch {
_state.value = ResultState.Loading
try {
val data = repository.fetchCommodityPrice(
commodityName = commodityName,
marketName = market,
date = date,
city = city
)
Log.d(TAG, "loadCommodityPrice: $commodityName, $market, $date, $city")
_state.value = ResultState.Success(data)
Log.d(TAG, "loadCommodityPrice: $data")
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_state.value = ResultState.Error(errorMessage)
Log.e(TAG, "loadCommodityPrice: $errorMessage", e)
}
}
}
}

View File

@ -0,0 +1,51 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.component
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.toRupiahFormat
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
@Composable
fun CommodityListItem(item: CommodityPriceResponseItem) {
ListItem(
modifier = Modifier.border(
shape = RoundedCornerShape(8.dp), width = 1.dp, color =
MaterialTheme.colorScheme.surfaceContainer
),
headlineContent = {
Text(
text = "${item.kab_kota} - ${item.pasar}",
style = MaterialTheme.typography.bodyMedium,
)
},
supportingContent = {
Text(
text = item.komoditas_nama.toString(),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alpha(0.5f)
)
},
trailingContent = {
Box(
modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(8.dp)).padding(vertical = 4.dp, horizontal = 8.dp)
)
{
Text(
text = "${item.harga?.toRupiahFormat()}",
style = MaterialTheme.typography.labelMedium
)
}
}
)
}

View File

@ -0,0 +1,329 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.R
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.percentageChange
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.toRupiahFormat
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.components.Loader
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
import java.util.Calendar
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PredictionScreen(
navController: NavHostController,
viewModel: PredictionViewModel = koinViewModel()
) {
val resultState by viewModel.commodityData.collectAsState()
val calendar = remember { Calendar.getInstance().apply { add(Calendar.DATE, -1) } }
val today = Date().toFormattedString()
val dateYesterday = calendar.time.toFormattedString()
val isRefreshing = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (resultState == ResultState.Idle) {
viewModel.loadCommodityPredictions(today)
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Price Prediction",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
)
}
) { innerPadding ->
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
viewModel.loadCommodityPredictions(today)
}
) {
when (resultState) {
is ResultState.Idle -> {
}
is ResultState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Loader()
}
}
is ResultState.Success -> {
isRefreshing.value = false
val data =
(resultState as ResultState.Success<List<CommodityPredictionResponse>>).data
if (!data.isNullOrEmpty()) {
var expanded by remember { mutableStateOf(false) }
var selectedCommodity by remember {
mutableStateOf<CommodityPredictionResponse?>(
data[0]
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) {
Box {
OutlinedButton(
onClick = { expanded = !expanded },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
border = BorderStroke(
color = MainGreen.copy(alpha = 0.5f),
width = 1.dp
)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = selectedCommodity?.nama_komoditas
?: "Pilih Komoditas",
color = MaterialTheme.colorScheme.onBackground
)
Image(
imageVector = Icons.Default.ArrowDropDown,
alpha = 0.5f,
contentDescription = "dropdown"
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
data.forEach { item ->
DropdownMenuItem(
text = {
Text(
item.nama_komoditas,
modifier = Modifier.fillMaxWidth()
)
},
colors = MenuDefaults.itemColors(MaterialTheme.colorScheme.onBackground),
onClick = {
selectedCommodity = item
expanded = false
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
selectedCommodity?.let { commodity ->
CommodityPredictionCard(commodity)
}
}
} else {
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Text("Data kosong.")
}
}
}
is ResultState.Error -> {
isRefreshing.value = false
val message = (resultState as ResultState.Error).message
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
contentAlignment = Alignment.Center
) {
DefaultErrorComponent(
label = "Oops!",
message = message
)
}
}
}
}
}
}
@Composable
private fun PredictedPriceListItem(
periode: String,
harga: String,
percentage: Double,
) {
ListItem(
supportingContent = {
Text(
harga,
style = MaterialTheme.typography.titleLarge
)
},
headlineContent = {
Text(
periode,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.alpha(0.5f)
)
},
trailingContent = {
val graphic: String = if (percentage >= 0) "" else ""
Text(
"$graphic ${"%.2f".format(percentage)}%",
style = MaterialTheme.typography.bodyMedium.copy(
color = if (percentage >= 0) MainGreen else Color.Red
)
)
}
)
}
@Composable
private fun CommodityPredictionCard(item: CommodityPredictionResponse) {
val hargaHariIni = item.harga?.toInt() ?: 0
val hargaBesok = item.val_1?.toInt() ?: 0
val hargaMinggu = item.val_7?.toInt() ?: 0
val hargaBulan = item.val_30?.toInt() ?: 0
val harga3Bulan = item.val_90?.toInt() ?: 0
Column(
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
"Komoditas",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.alpha(0.5f)
)
Text(item.nama_komoditas, style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Harga Hari Ini",
style = MaterialTheme.typography.bodySmall
)
Text(
"${item.harga?.toRupiahFormat()}",
style = MaterialTheme.typography.headlineSmall
)
}
Image(
painter = painterResource(
when (item.nama_komoditas) {
"Tomat Merah" -> R.drawable.tomat_merah
"Cabe Merah Besar" -> R.drawable.cabe_merah_besar
"Cabe Rawit Merah" -> R.drawable.cabe_rawit
"Bawang Merah" -> R.drawable.bawang_merah
"Bawang Putih Sinco/Honan" -> R.drawable.bawang_putih
else -> R.drawable.bawang_merah
}
),
contentDescription = "commodity",
modifier = Modifier.size(64.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
PredictedPriceListItem(
"Besok",
hargaBesok.toRupiahFormat(),
percentageChange(hargaHariIni, hargaBesok)
)
PredictedPriceListItem(
"Minggu Depan",
hargaMinggu.toRupiahFormat(),
percentageChange(hargaHariIni, hargaMinggu)
)
PredictedPriceListItem(
"Bulan Depan",
hargaBulan.toRupiahFormat(),
percentageChange(hargaHariIni, hargaBulan)
)
PredictedPriceListItem(
"3 Bulan ke Depan",
harga3Bulan.toRupiahFormat(),
percentageChange(hargaHariIni, harga3Bulan)
)
}
}

View File

@ -0,0 +1,40 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private const val TAG = "PredictionViewModel"
class PredictionViewModel(
private val repository: CommodityRepository
) : ViewModel() {
private val _commodityData = MutableStateFlow<ResultState<List<CommodityPredictionResponse>>>(
ResultState.Idle
)
val commodityData: StateFlow<ResultState<List<CommodityPredictionResponse>>> = _commodityData
fun loadCommodityPredictions(date: String) {
viewModelScope.launch {
_commodityData.value = ResultState.Loading
try {
val data = repository.fetchCommodityPredictions(date = date)
_commodityData.value = ResultState.Success(data)
Log.d(TAG, "loadCommodityPredictions: $data")
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_commodityData.value = ResultState.Error(errorMessage)
Log.e(TAG, "loadCommodityPredictions: $errorMessage")
}
}
}
}

View File

@ -0,0 +1,53 @@
package com.syaroful.agrilinkvocpro.control_feature.core.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun FeatureBanner(){
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = MainGreen, shape = RoundedCornerShape(8.dp))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.fillMaxWidth(0.6f)) {
Text(
"Kontrol Penyiraman",
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
)
Text(
"lakukan penyiraman hanya dengan sekali klik dari jarak jauh",
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
)
}
Image(
modifier = Modifier.fillMaxWidth(0.8f),
painter = painterResource(id = R.drawable.watering_img_banner),
contentDescription = "Plant Image",
)
}
}
}

View File

@ -0,0 +1,6 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable