From 5a0448f386be993133ac638a4ee8a03a11dc73b0 Mon Sep 17 00:00:00 2001 From: mtgriego Date: Tue, 5 Mar 2024 11:00:38 -0800 Subject: [PATCH] [MBL-1229] Add Compose Views for Add-ons screen (#1964) * WIP - add ons scrren and container * minor updates to add-ons * add more add on screen functionality, further testing needed when implemented * lint * add spacing at bottom of list --- .../compose/projectpage/AddOnsContainer.kt | 211 ++++++++++++ .../compose/projectpage/AddOnsScreen.kt | 320 ++++++++++++++++++ .../ui/compose/designsystem/KSDimensions.kt | 10 +- .../ui/compose/designsystem/KSStepper.kt | 8 +- 4 files changed, 543 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsContainer.kt create mode 100644 app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsScreen.kt diff --git a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsContainer.kt b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsContainer.kt new file mode 100644 index 0000000000..b22aeaf95d --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsContainer.kt @@ -0,0 +1,211 @@ +package com.kickstarter.ui.activities.compose.projectpage + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.kickstarter.ui.compose.designsystem.KSCoralBadge +import com.kickstarter.ui.compose.designsystem.KSDividerLineGrey +import com.kickstarter.ui.compose.designsystem.KSPrimaryBlackButton +import com.kickstarter.ui.compose.designsystem.KSStepper +import com.kickstarter.ui.compose.designsystem.KSTheme +import com.kickstarter.ui.compose.designsystem.KSTheme.colors +import com.kickstarter.ui.compose.designsystem.KSTheme.dimensions +import com.kickstarter.ui.compose.designsystem.KSTheme.typography +import com.kickstarter.ui.compose.designsystem.shapes + +@Composable +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun AddOnsContainerPreview() { + KSTheme { + AddOnsContainer( + title = "This Is A Test", + amount = "$500", + shippingAmount = "$5 shipping each", + conversionAmount = "About $500", + description = "This is just a test, don't worry about it, This is just a test, don't worry about it, This is just a test, don't worry about it, This is just a test, don't worry about it", + includesList = listOf("this is item 1", "this is item 2", "this is item 3"), + limit = 10, + buttonEnabled = true, + buttonText = "Add", + onItemAddedOrRemoved = { count -> + } + ) + } +} + +@Composable +fun AddOnsContainer( + title: String, + amount: String, + shippingAmount: String? = null, + conversionAmount: String? = null, + description: String, + includesList: List = listOf(), + limit: Int = -1, + buttonEnabled: Boolean, + buttonText: String, + onItemAddedOrRemoved: (count: Int) -> Unit +) { + var addOnCount by rememberSaveable { mutableStateOf(0) } + + Card( + modifier = Modifier.fillMaxWidth(), + backgroundColor = colors.kds_white, + shape = shapes.medium + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(dimensions.paddingMediumLarge) + ) { + + Text(text = title, style = typography.title2Bold, color = colors.kds_black) + + Row { + Text(text = amount, style = typography.callout, color = colors.textAccentGreen) + + if (!shippingAmount.isNullOrEmpty()) { + Text( + text = " + $shippingAmount", + style = typography.callout, + color = colors.textAccentGreen + ) + } + } + + if (!conversionAmount.isNullOrEmpty()) { + Text( + modifier = Modifier.padding(top = dimensions.paddingXSmall), + text = conversionAmount, + style = typography.footnoteMedium, + color = colors.textSecondary + ) + } + + Spacer(modifier = Modifier.height(dimensions.paddingMediumLarge)) + + Text(text = description, style = typography.body2, color = colors.textPrimary) + + Spacer(modifier = Modifier.height(dimensions.paddingMedium)) + + KSDividerLineGrey() + + Spacer(modifier = Modifier.height(dimensions.paddingMedium)) + + if (includesList.isNotEmpty()) { + Text( + text = "Includes", + style = typography.calloutMedium, + color = colors.textSecondary + ) + + includesList.forEachIndexed { index, itemDescription -> + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.width(dimensions.paddingMediumSmall)) + + Box( + modifier = Modifier + .padding(end = dimensions.paddingSmall) + .size(dimensions.dottedListDotSize) + .background(color = colors.textPrimary, shape = CircleShape), + ) + + Text( + text = itemDescription, + style = typography.body2, + color = colors.textPrimary + ) + + Spacer(modifier = Modifier.width(dimensions.paddingMediumSmall)) + } + + if (index != includesList.lastIndex) KSDividerLineGrey() + } + } + + if (limit > 0) { + Spacer(Modifier.height(dimensions.paddingMedium)) + + KSCoralBadge(text = "Limit $limit") + } + + Spacer(Modifier.height(dimensions.paddingLarge)) + + when (addOnCount) { + 0 -> { + KSPrimaryBlackButton( + onClickAction = { + addOnCount++ + onItemAddedOrRemoved(addOnCount) + }, + text = buttonText, + isEnabled = buttonEnabled + ) + } + else -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + KSStepper( + onPlusClicked = { + addOnCount++ + onItemAddedOrRemoved(addOnCount) + }, + isPlusEnabled = addOnCount < limit, + onMinusClicked = { + addOnCount-- + onItemAddedOrRemoved(addOnCount) + }, + isMinusEnabled = true + ) + + Box( + modifier = Modifier + .border( + width = dimensions.dividerThickness, + color = colors.textSecondary, + shape = shapes.small + ) + .padding( + top = dimensions.paddingSmall, + bottom = dimensions.paddingSmall, + start = dimensions.paddingMedium, + end = dimensions.paddingMedium + ) + ) { + Text( + text = "$addOnCount", + style = typography.callout, + color = colors.textPrimary + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsScreen.kt b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsScreen.kt new file mode 100644 index 0000000000..7cfc8c925d --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/AddOnsScreen.kt @@ -0,0 +1,320 @@ +package com.kickstarter.ui.activities.compose.projectpage + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Card +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import com.kickstarter.R +import com.kickstarter.libs.Environment +import com.kickstarter.models.Project +import com.kickstarter.models.Reward +import com.kickstarter.models.ShippingRule +import com.kickstarter.ui.compose.designsystem.KSPrimaryGreenButton +import com.kickstarter.ui.compose.designsystem.KSTheme +import com.kickstarter.ui.compose.designsystem.KSTheme.colors +import com.kickstarter.ui.compose.designsystem.KSTheme.dimensions +import com.kickstarter.ui.compose.designsystem.KSTheme.typography +import com.kickstarter.ui.compose.designsystem.shapes + +@Composable +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun AddOnsScreenPreview() { + KSTheme { + Scaffold( + backgroundColor = colors.backgroundAccentGraySubtle + ) { padding -> + AddOnsScreen( + modifier = Modifier.padding(padding), + environment = Environment.Builder().build(), + lazyColumnListState = rememberLazyListState(), + countryList = listOf(), + onShippingRuleSelected = {}, + rewardItems = (0..10).map { + Reward.builder() + .title("Item Number $it") + .description("This is a description for item $it") + .id(it.toLong()) + .convertedMinimum((100 * (it + 1)).toDouble()) + .isAvailable(it != 0) + .limit(if (it == 0) 1 else 10) + .build() + }, + project = + Project.builder() + .currency("USD") + .currentCurrency("USD") + .build(), + onItemAddedOrRemoved = {}, + onContinueClicked = {} + ) + } + } +} + +@Composable +fun AddOnsScreen( + modifier: Modifier, + environment: Environment, + lazyColumnListState: LazyListState, + initialCountryInput: String? = null, + countryList: List, + onShippingRuleSelected: (ShippingRule) -> Unit, + rewardItems: List, + project: Project, + onItemAddedOrRemoved: (Map) -> Unit, + onContinueClicked: () -> Unit +) { + + var countryInput by remember { + mutableStateOf(initialCountryInput ?: "United States") + } + var countryListExpanded by remember { + mutableStateOf(false) + } + val interactionSource = remember { + MutableInteractionSource() + } + var addOnCount by remember { + mutableStateOf(0) + } + val rewardSelections: MutableMap = mutableMapOf() + + Scaffold( + modifier = modifier, + bottomBar = { + Column { + Surface( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape( + topStart = dimensions.radiusLarge, + topEnd = dimensions.radiusLarge + ), + color = colors.backgroundSurfacePrimary, + elevation = dimensions.elevationLarge, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(dimensions.paddingMediumLarge) + ) { + KSPrimaryGreenButton( + onClickAction = onContinueClicked, + text = + if (addOnCount == 0) stringResource(id = R.string.Skip_add_ons) + else { + when { + addOnCount == 1 -> environment.ksString()?.format( + stringResource(R.string.Continue_with_quantity_count_add_ons_one), + "quantity_count", + addOnCount.toString() + ) ?: "" + + addOnCount > 1 -> environment.ksString()?.format( + stringResource(R.string.Continue_with_quantity_count_add_ons_many), + "quantity_count", + addOnCount.toString() + ) ?: "" + + else -> stringResource(id = R.string.Skip_add_ons) + } + }, + isEnabled = true + ) + } + } + } + }, + backgroundColor = colors.backgroundAccentGraySubtle + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding( + start = dimensions.paddingMedium, + end = dimensions.paddingMedium, + top = dimensions.paddingMedium + ) + .padding(paddingValues = padding), + state = lazyColumnListState + ) { + item { + Text( + text = stringResource(id = R.string.Customize_your_reward_with_optional_addons), + style = typography.title3Bold, + color = colors.textPrimary + ) + + Spacer(modifier = Modifier.height(dimensions.paddingMediumLarge)) + + Text( + text = stringResource(id = R.string.Your_shipping_location), + style = typography.subheadlineMedium, + color = colors.textSecondary + ) + + Spacer(modifier = Modifier.height(dimensions.paddingSmall)) + + Column( + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { countryListExpanded = false } + ), + ) { + TextField( + modifier = Modifier + .height(dimensions.minButtonHeight) + .width(dimensions.countryInputWidth), + value = countryInput, + onValueChange = { + countryInput = it + countryListExpanded = true + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + shape = shapes.medium, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = colors.kds_white, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + textStyle = typography.subheadlineMedium.copy(color = colors.textAccentGreenBold), + ) + + AnimatedVisibility(visible = countryListExpanded) { + Card(shape = shapes.medium) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .background(color = colors.kds_white) + ) { + if (countryInput.isNotEmpty()) { + items( + items = countryList.filter { + it.location()?.displayableName()?.lowercase() + ?.contains(countryInput.lowercase()) ?: false + } + ) { + CountryListItems( + item = it, + title = it.location()?.displayableName() ?: "", + onSelect = { country -> + countryInput = + country.location()?.displayableName() ?: "" + countryListExpanded = false + onShippingRuleSelected(country) + } + ) + } + } else { + items(countryList) { + CountryListItems( + item = it, + title = it.location()?.displayableName() ?: "", + onSelect = { country -> + countryInput = + country.location()?.displayableName() ?: "" + countryListExpanded = false + onShippingRuleSelected(country) + } + ) + } + } + } + } + } + } + } + + items( + items = rewardItems + ) { reward -> + Spacer(modifier = Modifier.height(dimensions.paddingMedium)) + + AddOnsContainer( + title = reward.title() ?: "", + amount = environment.ksCurrency()?.format( + reward.convertedMinimum(), + project, + true, + ) ?: "", + shippingAmount = "", // todo in implementation + description = reward.description() ?: "", + buttonEnabled = reward.isAvailable(), + buttonText = stringResource(id = R.string.Add), + limit = reward.limit() ?: -1, + onItemAddedOrRemoved = { count -> + rewardSelections[reward] = count + var totalRewardsCount = 0 + rewardSelections.forEach { + totalRewardsCount += it.value + } + addOnCount = totalRewardsCount + onItemAddedOrRemoved(rewardSelections) + } + ) + } + + item { + Spacer(modifier = Modifier.height(dimensions.paddingDoubleLarge)) + } + } + } +} + +@Composable +fun CountryListItems( + item: ShippingRule, + title: String, + onSelect: (ShippingRule) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(item) } + .padding(dimensions.paddingXSmall) + ) { + Text(text = title) + } +} diff --git a/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt b/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt index 0a55c25690..32f73eb573 100644 --- a/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt +++ b/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt @@ -42,6 +42,7 @@ data class KSDimensions( val dialogWidth: Dp = Dp.Unspecified, val dialogButtonSpacing: Dp = Dp.Unspecified, val elevationMedium: Dp = Dp.Unspecified, + val elevationLarge: Dp = Dp.Unspecified, val assistiveTextTopSpacing: Dp = Dp.Unspecified, val verticalDividerWidth: Dp = Dp.Unspecified, val iconSizeMedium: Dp = Dp.Unspecified, @@ -51,7 +52,9 @@ data class KSDimensions( val searchAppBarHeight: Dp = Dp.Unspecified, val appBarEndPadding: Dp = Dp.Unspecified, val appBarSearchPadding: Dp = Dp.Unspecified, - val clickableButtonHeight: Dp = Dp.Unspecified + val clickableButtonHeight: Dp = Dp.Unspecified, + val dottedListDotSize: Dp = Dp.Unspecified, + val countryInputWidth: Dp = Dp.Unspecified ) val LocalKSCustomDimensions = staticCompositionLocalOf { @@ -93,6 +96,7 @@ val KSStandardDimensions = KSDimensions( dialogWidth = 280.dp, dialogButtonSpacing = 2.dp, elevationMedium = 8.dp, + elevationLarge = 16.dp, assistiveTextTopSpacing = 6.dp, verticalDividerWidth = 4.dp, iconSizeMedium = 18.dp, @@ -102,5 +106,7 @@ val KSStandardDimensions = KSDimensions( searchAppBarHeight = 68.dp, appBarEndPadding = 12.dp, appBarSearchPadding = 6.dp, - clickableButtonHeight = 48.dp + clickableButtonHeight = 48.dp, + dottedListDotSize = 2.dp, + countryInputWidth = 164.dp ) diff --git a/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSStepper.kt b/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSStepper.kt index f4183f143b..3055c86cf7 100644 --- a/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSStepper.kt +++ b/app/src/main/java/com/kickstarter/ui/compose/designsystem/KSStepper.kt @@ -84,8 +84,8 @@ fun KSStepper( bottomEnd = dimensions.none ), colors = ButtonDefaults.buttonColors( - backgroundColor = colors.kds_white, - disabledBackgroundColor = colors.kds_support_300 + backgroundColor = colors.backgroundAccentGraySubtle, + disabledBackgroundColor = colors.backgroundActionDisabled ), onClick = onMinusClicked, enabled = isMinusEnabled, @@ -113,8 +113,8 @@ fun KSStepper( bottomEnd = dimensions.radiusMediumSmall ), colors = ButtonDefaults.buttonColors( - backgroundColor = colors.kds_white, - disabledBackgroundColor = colors.kds_support_300 + backgroundColor = colors.backgroundAccentGraySubtle, + disabledBackgroundColor = colors.backgroundActionDisabled ), onClick = onPlusClicked, enabled = isPlusEnabled,