Coverage Summary for Class: SurferIncomingInviteCardKt (com.georgeci.moneysurfer.uikit.components.workspace)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferIncomingInviteCardKt |
0%
(0/1)
|
0%
(0/5)
|
0%
(0/30)
|
0%
(0/21)
|
0%
(0/423)
|
package com.georgeci.moneysurfer.uikit.components.workspace
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.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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.atom.SurferCard
import com.georgeci.moneysurfer.uikit.components.SurferButton
import com.georgeci.moneysurfer.uikit.components.SurferButtonSize
import com.georgeci.moneysurfer.uikit.components.SurferButtonStyle
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
/**
* Card for an incoming workspace invite addressed to the current user. Shown on the
* "Invitations" screen.
*
* Layout: a small workspace-icon avatar + workspace name + inviter handle on the top row,
* with a role chip pinned to the trailing edge. Bottom row carries the action buttons
* (Decline / Accept) for active invites, or just an "Expired" tag for expired ones — no
* action buttons in the expired case (a separate cleanup path purges them).
*
* i18n-free by contract: callers pass already-resolved strings (workspace name, role label,
* button labels, expired label). Keeps uikit independent of the feature module's resource
* bundle.
*/
@Composable
fun SurferIncomingInviteCard(
workspaceName: String,
inviterLabel: String,
roleLabel: String,
expired: Boolean,
expiredLabel: String,
acceptLabel: String,
declineLabel: String,
onAccept: () -> Unit,
onDecline: () -> Unit,
modifier: Modifier = Modifier,
busy: Boolean = false,
) {
val cs = AppTheme.materialColors
SurferCard(modifier = modifier.fillMaxWidth()) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppTheme.spacing.medium),
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(cs.surfaceContainerHighest),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = SurferIcons.People,
contentDescription = SurferSemantics.Decorative,
tint = cs.onSurfaceVariant,
modifier = Modifier.size(18.dp),
)
}
Column(Modifier.weight(1f)) {
Text(
text = workspaceName,
style = AppTheme.typography.titleSmall,
color = cs.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = inviterLabel,
style = AppTheme.typography.bodySmall,
color = cs.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
RoleChip(roleLabel)
}
Spacer(Modifier.height(AppTheme.spacing.small))
if (expired) {
Text(
text = expiredLabel,
style = AppTheme.typography.labelMedium,
color = cs.error,
)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(AppTheme.spacing.small),
) {
Spacer(Modifier.weight(1f))
SurferButton(
text = declineLabel,
style = SurferButtonStyle.Text,
size = SurferButtonSize.Small,
enabled = !busy,
onClick = onDecline,
)
SurferButton(
text = acceptLabel,
style = SurferButtonStyle.Filled,
size = SurferButtonSize.Small,
startIcon = SurferIcons.Check,
enabled = !busy,
onClick = onAccept,
)
}
}
}
}
}
@Composable
private fun RoleChip(label: String) {
val cs = AppTheme.materialColors
Box(
modifier = Modifier
.height(24.dp)
.clip(RoundedCornerShape(12.dp))
.background(cs.secondaryContainer)
.padding(horizontal = 10.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = label,
style = AppTheme.typography.labelSmall,
color = cs.onSecondaryContainer,
)
}
}
@Preview
@Composable
private fun SurferIncomingInviteCardPreview_Active() {
SurferComponentPreview {
Column(modifier = Modifier.padding(16.dp)) {
SurferIncomingInviteCard(
workspaceName = "Lisbon trip '26",
inviterLabel = "@kasiam",
roleLabel = "Editor",
expired = false,
expiredLabel = "Expired",
acceptLabel = "Accept",
declineLabel = "Decline",
onAccept = {},
onDecline = {},
)
}
}
}
@Preview
@Composable
private fun SurferIncomingInviteCardPreview_Expired() {
SurferComponentPreview {
Column(modifier = Modifier.padding(16.dp)) {
SurferIncomingInviteCard(
workspaceName = "Office lunch club with a really long name that ellipses",
inviterLabel = "@marekw",
roleLabel = "Viewer",
expired = true,
expiredLabel = "Expired",
acceptLabel = "Accept",
declineLabel = "Decline",
onAccept = {},
onDecline = {},
)
}
}
}
@Preview
@Composable
private fun SurferIncomingInviteCardPreview_Busy() {
SurferComponentPreview {
Column(modifier = Modifier.padding(16.dp)) {
SurferIncomingInviteCard(
workspaceName = "Family",
inviterLabel = "@dadj",
roleLabel = "Editor",
expired = false,
expiredLabel = "Expired",
acceptLabel = "Accept",
declineLabel = "Decline",
onAccept = {},
onDecline = {},
busy = true,
)
}
}
}