Coverage Summary for Class: SurferRecurringItem (com.georgeci.moneysurfer.uikit.widgets)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferRecurringItem |
100%
(1/1)
|
100%
(1/1)
|
|
100%
(6/6)
|
100%
(35/35)
|
package com.georgeci.moneysurfer.uikit.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
import com.georgeci.moneysurfer.uikit.theme.AppTheme
data class SurferRecurringItem(
val id: String,
val name: String,
val dueLabel: String,
val amountFormatted: String,
val isImminent: Boolean = false,
val icon: ImageVector = SurferIcons.Calendar,
)
@Composable
fun SurferRecurringWidget(
items: List<SurferRecurringItem>,
title: String,
actionLabel: String,
onActionClick: () -> Unit,
modifier: Modifier = Modifier,
size: SurferWidgetSize = LocalSurferWidgetSize.current,
onItemClick: ((SurferRecurringItem) -> Unit)? = null,
subtitle: String? = null,
emptyTitle: String? = null,
emptySubtitle: String? = null,
) {
val hero = size == SurferWidgetSize.Expanded
val visibleItems = if (hero) items.take(3) else items.take(2)
Column(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(AppTheme.materialColors.surface)
.padding(vertical = 14.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = AppTheme.typography.titleMedium,
color = AppTheme.materialColors.onSurface,
)
if (subtitle != null) {
Spacer(Modifier.height(2.dp))
Text(
text = subtitle,
style = AppTheme.typography.bodySmall,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
}
Text(
text = actionLabel,
style = AppTheme.typography.labelMedium,
color = AppTheme.materialColors.primary,
modifier = Modifier.clickable(onClick = onActionClick),
)
}
if (items.isEmpty()) {
SurferWidgetEmptyState(
icon = SurferIcons.Calendar,
title = emptyTitle,
subtitle = emptySubtitle,
)
return@Column
}
Spacer(Modifier.height(8.dp))
visibleItems.forEach { item ->
RecurringRow(
item = item,
onClick = onItemClick?.let { handler -> { handler(item) } },
)
}
}
}
@Composable
private fun RecurringRow(item: SurferRecurringItem, onClick: (() -> Unit)?) {
val rowModifier = if (onClick != null) {
Modifier.fillMaxWidth().clickable(onClick = onClick)
} else {
Modifier.fillMaxWidth()
}
Row(
modifier = rowModifier
.background(
if (item.isImminent) {
AppTheme.materialColors.surfaceContainerLow
} else {
androidx.compose.ui.graphics.Color.Transparent
},
)
.padding(horizontal = 20.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(AppTheme.materialColors.surfaceContainerHigh),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = item.icon,
contentDescription = SurferSemantics.Decorative,
tint = AppTheme.materialColors.onSurfaceVariant,
modifier = Modifier.size(20.dp),
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.name,
style = AppTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
color = AppTheme.materialColors.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = item.dueLabel,
style = AppTheme.typography.bodySmall,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
Text(
text = item.amountFormatted,
style = AppTheme.typography.titleSmall,
color = AppTheme.materialColors.onSurface,
)
}
}
@Preview
@Composable
private fun SurferRecurringWidgetHeroPreview() {
SurferComponentPreview {
Box(modifier = Modifier.padding(16.dp)) {
SurferRecurringWidget(
items = listOf(
SurferRecurringItem("1", "Spotify", "Tomorrow", "€9.99", isImminent = true),
SurferRecurringItem("2", "Rent", "Apr 30", "€980"),
SurferRecurringItem("3", "Gym", "May 02", "€32"),
),
title = "Upcoming",
subtitle = "€1,021.99 due in next 7 days",
actionLabel = "Manage",
onActionClick = {},
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Preview
@Composable
private fun SurferRecurringWidgetEmptyPreview() {
SurferComponentPreview {
Box(modifier = Modifier.padding(16.dp)) {
SurferRecurringWidget(
items = emptyList(),
title = "Upcoming",
actionLabel = "Manage",
onActionClick = {},
emptyTitle = "Nothing scheduled",
emptySubtitle = "Subscriptions and recurring bills will appear here.",
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
)
}
}
}