diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml b/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..01e27bc --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_dining_out.xml b/composeApp/src/commonMain/composeResources/drawable/ic_dining_out.xml new file mode 100644 index 0000000..0f0a273 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_dining_out.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_education.xml b/composeApp/src/commonMain/composeResources/drawable/ic_education.xml new file mode 100644 index 0000000..78384bb --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_education.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_entertainment.xml b/composeApp/src/commonMain/composeResources/drawable/ic_entertainment.xml new file mode 100644 index 0000000..63cd7c3 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_entertainment.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_gifts.xml b/composeApp/src/commonMain/composeResources/drawable/ic_gifts.xml new file mode 100644 index 0000000..f9fda3b --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_gifts.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_groceries.xml b/composeApp/src/commonMain/composeResources/drawable/ic_groceries.xml new file mode 100644 index 0000000..570c5a0 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_groceries.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_health.xml b/composeApp/src/commonMain/composeResources/drawable/ic_health.xml new file mode 100644 index 0000000..5666fe7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_health.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_income.xml b/composeApp/src/commonMain/composeResources/drawable/ic_income.xml new file mode 100644 index 0000000..e85f110 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_income.xml @@ -0,0 +1,6 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_miscellaneous.xml b/composeApp/src/commonMain/composeResources/drawable/ic_miscellaneous.xml new file mode 100644 index 0000000..29b7166 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_miscellaneous.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_subscription.xml b/composeApp/src/commonMain/composeResources/drawable/ic_subscription.xml new file mode 100644 index 0000000..9449779 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_subscription.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_transport.xml b/composeApp/src/commonMain/composeResources/drawable/ic_transport.xml new file mode 100644 index 0000000..088aab1 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_transport.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_travel.xml b/composeApp/src/commonMain/composeResources/drawable/ic_travel.xml new file mode 100644 index 0000000..8849886 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_travel.xml @@ -0,0 +1,4 @@ + + + diff --git a/composeApp/src/commonMain/kotlin/data/datasource/local/LocalOperationDataSource.kt b/composeApp/src/commonMain/kotlin/data/datasource/local/LocalOperationDataSource.kt index c157c36..d2ec373 100644 --- a/composeApp/src/commonMain/kotlin/data/datasource/local/LocalOperationDataSource.kt +++ b/composeApp/src/commonMain/kotlin/data/datasource/local/LocalOperationDataSource.kt @@ -1,9 +1,10 @@ package data.datasource.local import com.github.guibrisson.db.MoneyMateDatabase -import com.github.guibrisson.db.OperationTable import data.datasource.OperationDataSource +import domain.model.Category import domain.model.Operation +import domain.model.OperationType import kotlinx.datetime.LocalDateTime import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -19,9 +20,9 @@ class LocalOperationDataSource: KoinComponent, OperationDataSource { id = -1, amount = 0.0, description = "Test", - type = "", - category = "", - date = LocalDateTime(2024, 3, 6, 1, 1, 1, 1), + type = OperationType.INCOME, + category = Category.EDUCATION, + date = LocalDateTime(2024, 3, 6, 1, 1 ,1, 1), isPeriodic = false, ) ) diff --git a/composeApp/src/commonMain/kotlin/domain/model/Category.kt b/composeApp/src/commonMain/kotlin/domain/model/Category.kt index f1c72b7..d605602 100644 --- a/composeApp/src/commonMain/kotlin/domain/model/Category.kt +++ b/composeApp/src/commonMain/kotlin/domain/model/Category.kt @@ -1,4 +1,59 @@ package domain.model -// TODO: implement properly -typealias Category = String +import androidx.compose.ui.graphics.Color +import moneymate.composeapp.generated.resources.Res +import moneymate.composeapp.generated.resources.ic_dining_out +import moneymate.composeapp.generated.resources.ic_education +import moneymate.composeapp.generated.resources.ic_entertainment +import moneymate.composeapp.generated.resources.ic_gifts +import moneymate.composeapp.generated.resources.ic_groceries +import moneymate.composeapp.generated.resources.ic_health +import moneymate.composeapp.generated.resources.ic_income +import moneymate.composeapp.generated.resources.ic_miscellaneous +import moneymate.composeapp.generated.resources.ic_subscription +import moneymate.composeapp.generated.resources.ic_transport +import moneymate.composeapp.generated.resources.ic_travel +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.ExperimentalResourceApi +import presentation.theme.amber500 +import presentation.theme.amber600 +import presentation.theme.blue500 +import presentation.theme.blue600 +import presentation.theme.cyan500 +import presentation.theme.cyan600 +import presentation.theme.emerald500 +import presentation.theme.emerald600 +import presentation.theme.indigo500 +import presentation.theme.indigo600 +import presentation.theme.lime500 +import presentation.theme.lime600 +import presentation.theme.orange500 +import presentation.theme.orange600 +import presentation.theme.pink500 +import presentation.theme.pink600 +import presentation.theme.purple500 +import presentation.theme.purple600 +import presentation.theme.red500 +import presentation.theme.red600 +import presentation.theme.rose500 +import presentation.theme.rose600 + +@OptIn(ExperimentalResourceApi::class) +enum class Category(val categoryName: String, val primaryColor: Color, + val secondaryColor: Color, val icon: DrawableResource) { + HEALTH_WELLNESS("Health & Wellness", red600, red500, Res.drawable.ic_health), + ENTERTAINMENT("Entertainment", orange600, orange500, Res.drawable.ic_entertainment), + SUBSCRIPTION("Subscription", amber500, amber600, Res.drawable.ic_subscription), + INCOME("Income", lime600, lime500, Res.drawable.ic_income), + DINING_OUT("Dining Out", emerald600, emerald500, Res.drawable.ic_dining_out), + EDUCATION("Education", cyan600, cyan500, Res.drawable.ic_education), + GROCERIES("Groceries", blue600, blue500, Res.drawable.ic_groceries), + TRAVEL("Travel", indigo500, indigo600, Res.drawable.ic_travel), + TRANSPORT("Transport", purple600, purple500, Res.drawable.ic_transport), + GIFTS_DONATIONS("Gifts & Donations", pink600, pink500, Res.drawable.ic_gifts), + MISCELLANEOUS("Miscellaneous", rose600, rose500, Res.drawable.ic_miscellaneous); + + companion object { + fun all(): Array = enumValues() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/domain/model/Operation.kt b/composeApp/src/commonMain/kotlin/domain/model/Operation.kt index df3040a..ea812b2 100644 --- a/composeApp/src/commonMain/kotlin/domain/model/Operation.kt +++ b/composeApp/src/commonMain/kotlin/domain/model/Operation.kt @@ -2,12 +2,11 @@ package domain.model import kotlinx.datetime.LocalDateTime - data class Operation( val id: Long, val amount: Double, val description: String, - val type: OperationType, + val type: OperationType, val category: Category, val date: LocalDateTime, val isPeriodic: Boolean, diff --git a/composeApp/src/commonMain/kotlin/domain/model/OperationType.kt b/composeApp/src/commonMain/kotlin/domain/model/OperationType.kt index e1033cb..443060a 100644 --- a/composeApp/src/commonMain/kotlin/domain/model/OperationType.kt +++ b/composeApp/src/commonMain/kotlin/domain/model/OperationType.kt @@ -1,4 +1,16 @@ package domain.model -// TODO: implement properly -typealias OperationType = String // "expense" || "income" +import androidx.compose.ui.graphics.Color +import presentation.theme.expense +import presentation.theme.income + +enum class OperationType(val operation: String, val color: Color) { + EXPENSE("Expense", expense), + INCOME("Income", income); + + companion object { + private fun all(): Array = enumValues() + + fun operationNames(): List = all().map { it.operation } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/presentation/designsystem/AppBars.kt b/composeApp/src/commonMain/kotlin/presentation/designsystem/AppBars.kt new file mode 100644 index 0000000..eb3064a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/presentation/designsystem/AppBars.kt @@ -0,0 +1,136 @@ +package presentation.designsystem + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.zIndex +import kotlinx.coroutines.launch +import presentation.theme.CORNER_RADIUS_4 +import presentation.theme.FONT_16 +import presentation.theme.SUPER_SMALL_PADDING +import presentation.theme.ZERO_DP +import presentation.theme.gray400 + +@Composable +fun TopAppBar(title: String, onBack: () -> Unit) { + TopAppBar( + contentColor = Color.White, + backgroundColor = Color.Transparent, + elevation = ZERO_DP, + ) { + Box(modifier = Modifier + .fillMaxSize() + .align(Alignment.CenterVertically) + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + text = title, + fontWeight = FontWeight.Bold, + fontSize = FONT_16, + textAlign = TextAlign.Center + ) + IconButton( + modifier = Modifier + .align(Alignment.CenterStart) + .fillMaxHeight(), + onClick = { onBack.invoke() } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back Button" + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TabBar(modifier: Modifier = Modifier, pages: List, + contentColors: List, onTabBarChange: (String) -> Unit) { + + val pagerState = rememberPagerState(pageCount = { pages.size }) + val coroutineScope = rememberCoroutineScope() + var selectedTabIndex by remember { + mutableStateOf(0) + } + + TabRow( + modifier = modifier + .clip(shape = RoundedCornerShape(CORNER_RADIUS_4)), + selectedTabIndex = selectedTabIndex, + indicator = { tabPositions -> + Box( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTabIndex]) + .zIndex(-1f) + .fillMaxWidth() + .fillMaxHeight() + .padding(SUPER_SMALL_PADDING) + .background( + color = contentColors[selectedTabIndex].copy(alpha = 0.12f), + shape = RoundedCornerShape(CORNER_RADIUS_4) + ) + ) + }, + contentColor = MaterialTheme.colors.secondary, + divider = {} + ) { + pages.forEachIndexed { index, title -> + Tab( + text = { + Text( + modifier = Modifier + .semantics { contentDescription = "Tab Title" }, + text = title, + color = + if (selectedTabIndex == index) + contentColors[selectedTabIndex] + else + gray400, + fontWeight = FontWeight.Bold + ) + }, + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + selectedTabIndex = index + onTabBarChange(pages[selectedTabIndex]) + } + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/presentation/designsystem/InputText.kt b/composeApp/src/commonMain/kotlin/presentation/designsystem/InputText.kt index 05f5dec..349f7d4 100644 --- a/composeApp/src/commonMain/kotlin/presentation/designsystem/InputText.kt +++ b/composeApp/src/commonMain/kotlin/presentation/designsystem/InputText.kt @@ -6,7 +6,12 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -30,7 +35,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import presentation.theme.FONT_32 +import presentation.theme.FONT_48 +import presentation.theme.INPUT_HEIGHT +import presentation.theme.MIN_INPUT_HEIGHT import presentation.theme.oswaldFontFamily import utils.MoneyVisualTransformation import utils.canBeLong @@ -70,7 +78,7 @@ private fun BaseInputText( ) { innerTextField -> Row( modifier = Modifier - .defaultMinSize(minHeight = 48.dp) + .defaultMinSize(minHeight = MIN_INPUT_HEIGHT) .clip(MaterialTheme.shapes.small) .background(MaterialTheme.colors.surface) .padding(12.dp), @@ -125,7 +133,7 @@ fun SearchInputText( BaseInputText( value = value, onValueChange = onValueChange, - modifier = modifier.height(52.dp), + modifier = modifier.height(INPUT_HEIGHT), singleLine = true, label = searchLabel, leadingIcon = leadingIcon, @@ -150,7 +158,7 @@ fun InputText( BaseInputText( value = value, onValueChange = onValueChange, - modifier = modifier.height(52.dp), + modifier = modifier.height(INPUT_HEIGHT), singleLine = true, label = basicLabel, ) @@ -172,15 +180,15 @@ fun MoneyInputText( Text( text = "$", fontFamily = oswaldFontFamily(), - fontSize = 32.sp, + fontSize = FONT_32, fontWeight = FontWeight.Light, ) val textStyle = TextStyle( - fontSize = 48.sp, + fontSize = FONT_48, fontFamily = oswaldFontFamily(), color = MaterialTheme.colors.onBackground, - lineHeight = 48.sp, + lineHeight = FONT_48, ) BasicTextField( diff --git a/composeApp/src/commonMain/kotlin/presentation/screen/operation/NewOperationScreen.kt b/composeApp/src/commonMain/kotlin/presentation/screen/operation/NewOperationScreen.kt index 40a2844..175fd26 100644 --- a/composeApp/src/commonMain/kotlin/presentation/screen/operation/NewOperationScreen.kt +++ b/composeApp/src/commonMain/kotlin/presentation/screen/operation/NewOperationScreen.kt @@ -1,48 +1,278 @@ package presentation.screen.operation -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.unit.dp +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import domain.model.Category +import domain.model.OperationType import moe.tlaster.precompose.koin.koinViewModel +import moneymate.composeapp.generated.resources.Res +import moneymate.composeapp.generated.resources.ic_arrow_right +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview +import presentation.designsystem.InputText +import presentation.designsystem.MoneyInputText +import presentation.designsystem.PrimaryButton +import presentation.designsystem.TabBar +import presentation.designsystem.TopAppBar +import presentation.theme.EXTRA_SMALL_PADDING +import presentation.theme.FONT_14 +import presentation.theme.FONT_16 +import presentation.theme.INPUT_HEIGHT +import presentation.theme.LARGE_PADDING +import presentation.theme.MEDIUM_PADDING import presentation.theme.MoneyMateTheme +import presentation.theme.SMALL_PADDING +import presentation.theme.border +import presentation.theme.expense +import presentation.theme.income @Composable fun NewOperationRoute( - modifier: Modifier = Modifier, onBack: () -> Unit, ) { val viewModel = koinViewModel(NewOperationViewModel::class) - NewOperationScreen(modifier = modifier, onBack = onBack) + NewOperationScreen(onBack = onBack) } +@OptIn(ExperimentalResourceApi::class) @Composable internal fun NewOperationScreen( - modifier: Modifier = Modifier, onBack: () -> Unit, ) { - Column(modifier = modifier.fillMaxSize().padding(20.dp)) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - IconButton(modifier = Modifier.offset(x = (-16).dp), onClick = onBack) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Arrow back") + var operationAmount by remember { + mutableStateOf("") + } + + var description by remember { + mutableStateOf("") + } + + var category: Category? by rememberSaveable { + mutableStateOf(Category.DINING_OUT) + } + + var selectedOperation by rememberSaveable { + mutableStateOf(OperationType.EXPENSE) + } + + var isMonthlyOperation by rememberSaveable { + mutableStateOf(false) + } + + val tabContentColors = listOf(expense, income) + val operations = OperationType.operationNames().toList() + + //TODO: Remover esta lista ao implementar lógica de navegação para tela de categorias + val categories = Category.all() + + Scaffold( + topBar = { + TopAppBar(title = "New Operation", onBack = onBack) + } + ) { + Column( + modifier = Modifier + .padding(horizontal = LARGE_PADDING) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + TabBar( + modifier = Modifier + .padding(vertical = LARGE_PADDING), + pages = operations, + contentColors = tabContentColors + ) { selectedItem -> + selectedOperation = OperationType.valueOf(selectedItem.uppercase()) + } + + Text("Add ${selectedOperation.operation.lowercase()}") + + MoneyInputText( + value = operationAmount, + onValueChange = { operationAmount = it } + ) + + Text(modifier = Modifier + .padding(top = LARGE_PADDING), + text = "Description") + + InputText( + modifier = Modifier + .padding(top = EXTRA_SMALL_PADDING), + value = description, + label = "e.g Hamburger from Bobs", + onValueChange = { + description = it + } + ) + + Text( + modifier = Modifier + .padding(top = LARGE_PADDING), + text = "Category" + ) + + Selector( + modifier = Modifier + .padding(top = EXTRA_SMALL_PADDING) + .fillMaxWidth(), + icon = category?.icon, + iconTint = category?.primaryColor, + value = category?.categoryName, + label = "category" + ) { + //TODO: Call Categories Screen + //Remover este código ao implementar navegação para tela de categorias + //OBS: Tela de categorias precisa ter um callback para atualizar a category + //desta tela. Remover também a lista categories + val randomInt = (0..9).random() + category = categories[randomInt] + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = MEDIUM_PADDING), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ){ + Text(text = "Is this a monthly operation?") + + CustomSwitch { + isMonthlyOperation = it + } + } + + Spacer(Modifier.weight(1f)) + + val enabled = operationAmount.isNotEmpty() && + description.isNotEmpty() && category != null + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = MEDIUM_PADDING), + onClick = { }, + enabled = enabled, + ) { + Text(text = "Done", style = MaterialTheme.typography.button) } + } + } +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun Selector(modifier: Modifier = Modifier, icon: DrawableResource? = null, + iconTint: Color? = null, value: String? = null, label: String, + onSelectorClick: () -> Unit +) { + Row( + modifier = modifier + .height(INPUT_HEIGHT) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colors.surface) + .clickable { onSelectorClick() } + .padding(SMALL_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(EXTRA_SMALL_PADDING) + ) { + icon?.let { + Icon( + painter = painterResource(it), + contentDescription = "Category Icon", + tint = iconTint ?: Color.White + ) + } - Text(text = "New operation") + value?.let { + Text( + text = it, + fontSize = FONT_16 + ) } + + Spacer(Modifier.weight(1f)) + + Text( + text = "Select a $label", + fontSize = FONT_14, + color = Color.White.copy(alpha = 0.6f) + ) + + Icon( + painterResource(Res.drawable.ic_arrow_right), + contentDescription = null + ) } } +@Composable +fun CustomSwitch(onChecked: (Boolean) -> Unit) { + + var isChecked by rememberSaveable { + mutableStateOf(false) + } + + Switch( + modifier = Modifier + .semantics { + contentDescription = "True or false selector" + }, + checked = isChecked, + onCheckedChange = { checked -> + isChecked = checked + onChecked(isChecked) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + checkedTrackColor = MaterialTheme.colors.surface, + uncheckedThumbColor = border, + uncheckedTrackColor = MaterialTheme.colors.surface + ) + ) +} + @Composable @Preview private fun PreviewNewOperationScreen() { MoneyMateTheme { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { NewOperationScreen(onBack = { }) } } -} +} diff --git a/composeApp/src/commonMain/kotlin/presentation/theme/Colors.kt b/composeApp/src/commonMain/kotlin/presentation/theme/Colors.kt index ea1be9a..9778f66 100644 --- a/composeApp/src/commonMain/kotlin/presentation/theme/Colors.kt +++ b/composeApp/src/commonMain/kotlin/presentation/theme/Colors.kt @@ -46,3 +46,6 @@ val pink500 = Color(0xffec4899) val pink600 = Color(0xffdb2777) val rose500 = Color(0xfff43f5e) val rose600 = Color(0xffe11d48) +val gray400 = Color(0xFFBBBBBB) +val expense = Color(0xFFEF7457) +val income = Color(0xFF26E0A9) diff --git a/composeApp/src/commonMain/kotlin/presentation/theme/Dimens.kt b/composeApp/src/commonMain/kotlin/presentation/theme/Dimens.kt new file mode 100644 index 0000000..2e33d50 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/presentation/theme/Dimens.kt @@ -0,0 +1,21 @@ +package presentation.theme + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +val LARGE_PADDING = 24.dp +val MEDIUM_PADDING = 16.dp +val SMALL_PADDING = 12.dp +val EXTRA_SMALL_PADDING = 8.dp +val SUPER_SMALL_PADDING = 2.dp +val ZERO_DP = 0.dp + +val CORNER_RADIUS_4 = 4.dp + +val FONT_14 = 14.sp +val FONT_16 = 16.sp +val FONT_32 = 32.sp +val FONT_48 = 48.sp + +val MIN_INPUT_HEIGHT = 48.dp +val INPUT_HEIGHT = 52.dp \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/presentation/theme/Type.kt b/composeApp/src/commonMain/kotlin/presentation/theme/Type.kt index b9fcc11..d19f0c5 100644 --- a/composeApp/src/commonMain/kotlin/presentation/theme/Type.kt +++ b/composeApp/src/commonMain/kotlin/presentation/theme/Type.kt @@ -1,16 +1,21 @@ package presentation.theme -import androidx.compose.runtime.Composable import androidx.compose.material.Typography +import androidx.compose.runtime.Composable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import moneymate.composeapp.generated.resources.* import moneymate.composeapp.generated.resources.Res +import moneymate.composeapp.generated.resources.inter_bold import moneymate.composeapp.generated.resources.inter_light import moneymate.composeapp.generated.resources.inter_medium import moneymate.composeapp.generated.resources.inter_regular +import moneymate.composeapp.generated.resources.inter_semibold +import moneymate.composeapp.generated.resources.oswald_bold +import moneymate.composeapp.generated.resources.oswald_light +import moneymate.composeapp.generated.resources.oswald_medium +import moneymate.composeapp.generated.resources.oswald_regular +import moneymate.composeapp.generated.resources.oswald_semibold import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.Font @@ -40,8 +45,8 @@ fun getTypography(): Typography { defaultFontFamily = interFontFamily(), button = TextStyle( fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 16.sp, + fontSize = FONT_14, + lineHeight = FONT_16, ) ) } diff --git a/composeApp/src/commonTest/kotlin/presentation/designsystem/CustomSwitchTest.kt b/composeApp/src/commonTest/kotlin/presentation/designsystem/CustomSwitchTest.kt new file mode 100644 index 0000000..3521acb --- /dev/null +++ b/composeApp/src/commonTest/kotlin/presentation/designsystem/CustomSwitchTest.kt @@ -0,0 +1,30 @@ +package presentation.designsystem + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import presentation.screen.operation.CustomSwitch +import kotlin.test.Test +import kotlin.test.assertEquals + +class CustomSwitchTest { + private val contentDescription = "True or false selector" + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Assert switch is true or false when perform two clicks`() = runComposeUiTest { + var selected = false + setContent { + CustomSwitch { + selected = it + } + } + + onNodeWithContentDescription(contentDescription).assertExists() + onNodeWithContentDescription(contentDescription).performClick() + assertEquals(selected, true) + onNodeWithContentDescription(contentDescription).performClick() + assertEquals(selected, false) + } +} \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/presentation/designsystem/TabBarTest.kt b/composeApp/src/commonTest/kotlin/presentation/designsystem/TabBarTest.kt new file mode 100644 index 0000000..ec766ae --- /dev/null +++ b/composeApp/src/commonTest/kotlin/presentation/designsystem/TabBarTest.kt @@ -0,0 +1,48 @@ +package presentation.designsystem + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import domain.model.OperationType +import presentation.theme.expense +import presentation.theme.income +import kotlin.test.Test + +class TabBarTest { + + private val tabContentColors = listOf(expense, income) + private val tabItems = OperationType.operationNames() + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Perform click on first tab item`() = runComposeUiTest { + var selectedItem = "" + + setContent { + TabBar( + pages = tabItems, + contentColors = tabContentColors + ) { selectedItem = it } + } + onNodeWithText(tabItems[0]).performClick() + onNodeWithText(tabItems[0]).assertTextEquals(selectedItem) + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Perform click on second tab item`() = runComposeUiTest { + var selectedItem = "" + + setContent { + + TabBar( + pages = tabItems, + contentColors = tabContentColors + ) { selectedItem = it } + } + onNodeWithText(tabItems[1]).performClick() + onNodeWithText(tabItems[1]).assertTextEquals(selectedItem) + } +} \ No newline at end of file