From 56f2d8c0455e5159137a13344ea047a3c75aebf5 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 16 Jun 2026 03:25:03 -0500 Subject: [PATCH] fix(home): update HomeScreen and HomeViewModel --- .../couplesconnect/app/ui/home/HomeScreen.kt | 665 ++++++++---------- .../app/ui/home/HomeViewModel.kt | 159 ++++- 2 files changed, 442 insertions(+), 382 deletions(-) diff --git a/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt index 123174e3..ee1ba81f 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/home/HomeScreen.kt @@ -1,12 +1,5 @@ package com.couplesconnect.app.ui.home -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -19,18 +12,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Favorite import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -41,7 +32,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -51,11 +41,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.couplesconnect.app.core.navigation.AppRoute -import com.couplesconnect.app.domain.model.LocalAnswer import com.couplesconnect.app.domain.model.Question import com.couplesconnect.app.domain.model.QuestionCategory -import com.couplesconnect.app.ui.answers.revealSummary -import com.couplesconnect.app.ui.components.SpecialDatesSection import com.couplesconnect.app.ui.questions.displayCategoryName @Composable @@ -93,7 +80,7 @@ private fun HomeContent( .fillMaxSize() .background( Brush.linearGradient( - listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)), + listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)), start = Offset.Zero, end = Offset.Infinite ) @@ -117,37 +104,42 @@ private fun HomeContent( state.isLoading -> LoadingHomeCard() state.error != null -> ErrorHomeCard(message = state.error, onRefresh = onRefresh) else -> { - TodayOverviewCard( - question = state.dailyQuestion, - stats = state.answerStats, - onDailyQuestion = onDailyQuestion, - onHistory = onHistory, - onPacks = onPacks - ) - SpecialDatesSection(compact = true) - LatestAnswerCard( - latest = state.answerStats.latest, - onHistory = onHistory + val onActionSelected: (HomeAction) -> Unit = { action -> + when (action.target) { + HomeActionTarget.InvitePartner -> onInvite() + HomeActionTarget.DailyQuestion -> onDailyQuestion() + HomeActionTarget.AnswerHistory -> onHistory() + HomeActionTarget.QuestionPacks -> { + action.categoryId?.let(onCategory) ?: onPacks() + } + HomeActionTarget.Settings -> onSettings() + } + } + + state.primaryAction?.let { action -> + PrimaryHomeActionCard( + action = action, + stats = state.answerStats, + streakCount = state.streakCount, + onAction = onActionSelected + ) + } + + ActionFeedSection( + actions = state.secondaryActions, + onAction = onActionSelected ) + + MomentCueCard() + CategoryPreviewGrid( categories = state.categories, onCategory = onCategory, onPacks = onPacks ) - SettingsStrip(onSettings = onSettings) } } } - - if (!state.isPaired && !state.isLoading) { - PulsingInviteFab( - onClick = onInvite, - modifier = Modifier - .align(Alignment.BottomEnd) - .navigationBarsPadding() - .padding(24.dp) - ) - } } } @@ -163,20 +155,20 @@ private fun HomeHeader( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Tonight’s connection", + text = "For tonight", style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), color = Color(0xFF27211F), modifier = Modifier.weight(1f) ) if (streakCount > 0) { - HomePill("$streakCount day streak") + HomePill("$streakCount nights showing up") } } Text( text = if (partnerName != null) - "Connected with $partnerName. Keep the conversation going." + "Connected with $partnerName. One clear next step, then the rest can stay quiet." else - "A quiet home for today’s prompt, saved reflections, and the next conversation worth opening.", + "Open the app, see what matters, and take one small step toward closeness.", style = MaterialTheme.typography.bodyLarge, color = Color(0xFF4E4642) ) @@ -184,115 +176,30 @@ private fun HomeHeader( } @Composable -private fun PulsingInviteFab( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val infiniteTransition = rememberInfiniteTransition(label = "fab_pulse") - - val ring1Scale by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 2.2f, - animationSpec = infiniteRepeatable( - animation = tween(1400, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "ring1" - ) - val ring1Alpha by infiniteTransition.animateFloat( - initialValue = 0.6f, - targetValue = 0f, - animationSpec = infiniteRepeatable( - animation = tween(1400, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "ring1a" - ) - val ring2Scale by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 2.2f, - animationSpec = infiniteRepeatable( - animation = tween(1400, delayMillis = 500, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "ring2" - ) - val ring2Alpha by infiniteTransition.animateFloat( - initialValue = 0.6f, - targetValue = 0f, - animationSpec = infiniteRepeatable( - animation = tween(1400, delayMillis = 500, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "ring2a" - ) - val fabScale by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 1.08f, - animationSpec = infiniteRepeatable( - animation = tween(700, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "fabscale" - ) - - Box( - modifier = modifier.size(72.dp), - contentAlignment = Alignment.Center - ) { - // Expanding ring 1 - Box( - modifier = Modifier - .size(56.dp) - .scale(ring1Scale) - .background( - color = Color(0xFFB98AF4).copy(alpha = ring1Alpha), - shape = CircleShape - ) - ) - // Expanding ring 2 (offset start) - Box( - modifier = Modifier - .size(56.dp) - .scale(ring2Scale) - .background( - color = Color(0xFFB98AF4).copy(alpha = ring2Alpha), - shape = CircleShape - ) - ) - // FAB - FloatingActionButton( - onClick = onClick, - modifier = Modifier.size(56.dp).scale(fabScale), - containerColor = Color(0xFFB98AF4), - contentColor = Color(0xFF271236), - shape = CircleShape - ) { - Icon( - Icons.Filled.Add, - contentDescription = "Invite partner", - modifier = Modifier.size(28.dp) - ) - } - } -} - -@Composable -private fun TodayOverviewCard( - question: Question?, +private fun PrimaryHomeActionCard( + action: HomeAction, stats: HomeAnswerStats, - onDailyQuestion: () -> Unit, - onHistory: () -> Unit, - onPacks: () -> Unit + streakCount: Int, + onAction: (HomeAction) -> Unit ) { + val colors = action.tone.actionColors() + Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(30.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.9f)), - elevation = CardDefaults.cardElevation(defaultElevation = 14.dp) + shape = RoundedCornerShape(32.dp), + colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.92f)), + elevation = CardDefaults.cardElevation(defaultElevation = 18.dp) ) { Column( - modifier = Modifier.padding(20.dp), + modifier = Modifier + .background( + Brush.linearGradient( + listOf(Color.White, colors.soft, Color(0xFFFFF7FB)), + start = Offset.Zero, + end = Offset.Infinite + ) + ) + .padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Row( @@ -300,104 +207,104 @@ private fun TodayOverviewCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - HomePill("Daily ritual") - question?.let { HomePill(it.category.displayCategoryName()) } + HomePill(action.eyebrow) + action.metric?.let { HomePill(it) } } - Text( - text = question?.text ?: "Your next question is ready.", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = Color(0xFF27211F), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(22.dp), - color = Color(0xFFF8F0FF) + Row( + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.Top ) { - Column( - modifier = Modifier.padding(14.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + Surface( + shape = RoundedCornerShape(20.dp), + color = colors.accent.copy(alpha = 0.16f), + modifier = Modifier.size(52.dp) ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Next best action", - style = MaterialTheme.typography.labelLarge, - color = Color(0xFF6B4A86), - fontWeight = FontWeight.SemiBold - ) - Text( - text = if (stats.private > 0) "${stats.private} private" else "${stats.total} saved", - style = MaterialTheme.typography.labelMedium, - color = Color(0xFF6B4A86) - ) - } - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - OverviewMetric( - label = "Saved", - value = stats.total.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory - ) - OverviewMetric( - label = "Revealed", - value = stats.revealed.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory - ) - OverviewMetric( - label = "Private", - value = stats.private.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = null, + tint = colors.deep, + modifier = Modifier.size(26.dp) ) } } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = action.title, + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + Text( + text = action.body, + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF4D4354), + maxLines = 4, + overflow = TextOverflow.Ellipsis + ) + } } - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( - onClick = onDailyQuestion, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFFB98AF4), - contentColor = Color(0xFF271236) + HomePulseStrip(stats = stats, streakCount = streakCount) + + Button( + onClick = { onAction(action) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = colors.accent, + contentColor = colors.onAccent ) - ) { - Text("Answer") - } - OutlinedButton( - onClick = onPacks, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp) - ) { - Text("Packs") - } + ) { + Text(action.cta) } } } } @Composable -private fun OverviewMetric( +private fun HomePulseStrip( + stats: HomeAnswerStats, + streakCount: Int +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + PulseMetric( + label = "Saved", + value = stats.total.toString(), + modifier = Modifier.weight(1f) + ) + PulseMetric( + label = "Private", + value = stats.private.toString(), + modifier = Modifier.weight(1f) + ) + PulseMetric( + label = "Nights", + value = streakCount.toString(), + modifier = Modifier.weight(1f) + ) + } +} + +@Composable +private fun PulseMetric( label: String, value: String, - modifier: Modifier = Modifier, - onClick: () -> Unit + modifier: Modifier = Modifier ) { - Card( - onClick = onClick, + Surface( modifier = modifier, - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.72f)), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + shape = RoundedCornerShape(18.dp), + color = Color.White.copy(alpha = 0.66f) ) { Column( modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), @@ -405,14 +312,14 @@ private fun OverviewMetric( ) { Text( text = value, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold), - color = Color(0xFF5F3A87), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF56336F), maxLines = 1 ) Text( text = label, style = MaterialTheme.typography.labelMedium, - color = Color(0xFF4E4642), + color = Color(0xFF5A5060), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -421,169 +328,186 @@ private fun OverviewMetric( } @Composable -private fun DailyQuestionCard( - question: Question?, - onDailyQuestion: () -> Unit, - onPacks: () -> Unit +private fun ActionFeedSection( + actions: List, + onAction: (HomeAction) -> Unit ) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(30.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.88f)), - elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) - ) { - Column( - modifier = Modifier.padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - HomePill("Daily ritual") - question?.let { HomePill(it.category.displayCategoryName()) } - } - Text( - text = question?.text ?: "Your next question is ready.", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = Color(0xFF27211F) - ) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( - onClick = onDailyQuestion, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFFB98AF4), - contentColor = Color(0xFF271236) - ) - ) { - Text("Open") - } - OutlinedButton( - onClick = onPacks, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp) - ) { - Text("Packs") - } - } + if (actions.isEmpty()) return + + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = "After that", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF2C2233) + ) + actions.forEach { action -> + SecondaryHomeActionCard(action = action, onAction = onAction) } } } @Composable -private fun AnswerStatsRow( - stats: HomeAnswerStats, - onHistory: () -> Unit +private fun SecondaryHomeActionCard( + action: HomeAction, + onAction: (HomeAction) -> Unit ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - StatCard( - label = "Saved", - value = stats.total.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory - ) - StatCard( - label = "Revealed", - value = stats.revealed.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory - ) - StatCard( - label = "Private", - value = stats.private.toString(), - modifier = Modifier.weight(1f), - onClick = onHistory - ) - } -} + val colors = action.tone.actionColors() -@Composable -private fun StatCard( - label: String, - value: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { Card( - onClick = onClick, - modifier = modifier, - shape = RoundedCornerShape(22.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.82f)), + onClick = { onAction(action) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { - Column( - modifier = Modifier.padding(14.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(13.dp), + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = value, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - color = Color(0xFF5F3A87) - ) - Text( - text = label, - style = MaterialTheme.typography.labelMedium, - color = Color(0xFF4E4642), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Composable -private fun LatestAnswerCard( - latest: LocalAnswer?, - onHistory: () -> Unit -) { - Card( - onClick = onHistory, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(26.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.82f)) - ) { - Column( - modifier = Modifier.padding(18.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - HomePill("Latest reflection") - latest?.let { HomePill(if (it.isRevealed) "Revealed" else "Private") } + Box( + modifier = Modifier + .size(40.dp) + .background(colors.soft, RoundedCornerShape(15.dp)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = null, + tint = colors.deep, + modifier = Modifier.size(20.dp) + ) } - Text( - text = latest?.questionText ?: "Your reflections will appear here after you answer a prompt.", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = Color(0xFF27211F), - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - latest?.let { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { Text( - text = if (it.isRevealed) it.revealSummary() else "Saved privately. Reveal it when the moment feels right.", - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF4E4642), + text = action.eyebrow, + style = MaterialTheme.typography.labelMedium, + color = colors.deep, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = action.title, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = action.body, + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF5A5060), maxLines = 2, overflow = TextOverflow.Ellipsis ) } + Surface( + shape = RoundedCornerShape(14.dp), + color = colors.soft + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = action.cta, + tint = colors.deep, + modifier = Modifier + .padding(9.dp) + .size(18.dp) + ) + } } } } +@Composable +private fun MomentCueCard() { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + color = Color(0xFFFFF8FC) + ) { + Column( + modifier = Modifier.padding(17.dp), + verticalArrangement = Arrangement.spacedBy(7.dp) + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + HomePill("Special dates") + HomePill("Quiet reminder") + } + Text( + text = "Keep important days close without turning them into chores.", + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E) + ) + Text( + text = "Birthdays, anniversaries, and planned moments will sit here as gentle cues once they are saved.", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF5A5060) + ) + } + } +} + +private data class HomeActionColors( + val soft: Color, + val accent: Color, + val deep: Color, + val onAccent: Color = Color(0xFF24122F) +) + +private fun HomeActionTone.actionColors(): HomeActionColors = + when (this) { + HomeActionTone.Invite -> HomeActionColors( + soft = Color(0xFFF4E8FF), + accent = Color(0xFFB98AF4), + deep = Color(0xFF56306F) + ) + HomeActionTone.Daily -> HomeActionColors( + soft = Color(0xFFFFE9F4), + accent = Color(0xFFE7A2D1), + deep = Color(0xFF6D2B55) + ) + HomeActionTone.Reflection -> HomeActionColors( + soft = Color(0xFFF0E8FF), + accent = Color(0xFFA98FE8), + deep = Color(0xFF4B3279) + ) + HomeActionTone.Ritual -> HomeActionColors( + soft = Color(0xFFEAF6F0), + accent = Color(0xFF9BC9AE), + deep = Color(0xFF28533A) + ) + HomeActionTone.Starter -> HomeActionColors( + soft = Color(0xFFFFF0E6), + accent = Color(0xFFEFC39D), + deep = Color(0xFF673D20) + ) + HomeActionTone.Pack -> HomeActionColors( + soft = Color(0xFFEAF0FF), + accent = Color(0xFF99AEE8), + deep = Color(0xFF2D407A) + ) + HomeActionTone.Utility -> HomeActionColors( + soft = Color(0xFFF0EDF3), + accent = Color(0xFFB7AFC0), + deep = Color(0xFF423849) + ) + } + @Composable private fun CategoryPreviewGrid( categories: List, onCategory: (String) -> Unit, onPacks: () -> Unit ) { + val featuredCategories = categories.take(2) + if (featuredCategories.isEmpty()) return + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Row( modifier = Modifier.fillMaxWidth(), @@ -591,20 +515,20 @@ private fun CategoryPreviewGrid( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Question packs", - style = MaterialTheme.typography.titleLarge, - color = Color(0xFF27211F), + text = "More doorways", + style = MaterialTheme.typography.titleMedium, + color = Color(0xFF2C2233), fontWeight = FontWeight.SemiBold ) OutlinedButton( onClick = onPacks, shape = RoundedCornerShape(14.dp) ) { - Text("All") + Text("All packs") } } - categories.chunked(2).forEach { rowItems -> + featuredCategories.chunked(2).forEach { rowItems -> Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { rowItems.forEach { item -> CategoryMiniCard( @@ -655,17 +579,6 @@ private fun CategoryMiniCard( } } -@Composable -private fun SettingsStrip(onSettings: () -> Unit) { - OutlinedButton( - onClick = onSettings, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) - ) { - Text("Settings") - } -} - @Composable private fun LoadingHomeCard() { Card( diff --git a/app/src/main/java/com/couplesconnect/app/ui/home/HomeViewModel.kt b/app/src/main/java/com/couplesconnect/app/ui/home/HomeViewModel.kt index cff81947..cc605a6a 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/home/HomeViewModel.kt @@ -27,7 +27,37 @@ data class HomeAnswerStats( val total: Int = 0, val revealed: Int = 0, val private: Int = 0, - val latest: LocalAnswer? = null + val latest: LocalAnswer? = null, + val answeredQuestionIds: Set = emptySet() +) + +enum class HomeActionTarget { + InvitePartner, + DailyQuestion, + AnswerHistory, + QuestionPacks, + Settings +} + +enum class HomeActionTone { + Invite, + Daily, + Reflection, + Ritual, + Starter, + Pack, + Utility +} + +data class HomeAction( + val eyebrow: String, + val title: String, + val body: String, + val cta: String, + val target: HomeActionTarget, + val tone: HomeActionTone, + val metric: String? = null, + val categoryId: String? = null ) data class HomeUiState( @@ -38,7 +68,9 @@ data class HomeUiState( val answerStats: HomeAnswerStats = HomeAnswerStats(), val partnerName: String? = null, val streakCount: Int = 0, - val isPaired: Boolean = false + val isPaired: Boolean = false, + val primaryAction: HomeAction? = null, + val secondaryActions: List = emptyList() ) @HiltViewModel @@ -84,14 +116,14 @@ class HomeViewModel @Inject constructor( partnerName = partnerName, streakCount = couple?.streakCount ?: 0, isPaired = couple != null - ) + ).withHomeActions() } } catch (e: Exception) { _uiState.update { it.copy( isLoading = false, error = e.message ?: "Could not load your dashboard." - ) + ).withHomeActions() } } } @@ -107,11 +139,126 @@ class HomeViewModel @Inject constructor( total = answers.size, revealed = answers.count { answer -> answer.isRevealed }, private = answers.count { answer -> !answer.isRevealed }, - latest = sorted.firstOrNull() + latest = sorted.firstOrNull(), + answeredQuestionIds = answers.map { answer -> answer.questionId }.toSet() ) - ) + ).withHomeActions() } } } } + + private fun HomeUiState.withHomeActions(): HomeUiState { + if (isLoading || error != null) { + return copy(primaryAction = null, secondaryActions = emptyList()) + } + + val primary = buildPrimaryAction() + return copy( + primaryAction = primary, + secondaryActions = buildSecondaryActions(primary) + ) + } + + private fun HomeUiState.buildPrimaryAction(): HomeAction { + val dailyQuestionIsUnanswered = dailyQuestion + ?.id + ?.let { questionId -> questionId !in answerStats.answeredQuestionIds } + ?: false + + return when { + !isPaired -> HomeAction( + eyebrow = "Next best action", + title = "Invite your partner into tonight.", + body = "The app works best as a shared ritual. Send a private invite and make the next prompt something you can both answer.", + cta = "Invite partner", + target = HomeActionTarget.InvitePartner, + tone = HomeActionTone.Invite + ) + + dailyQuestionIsUnanswered -> HomeAction( + eyebrow = "Tonight's prompt", + title = dailyQuestion?.text ?: "Answer tonight's question.", + body = "Start with one honest answer. You can keep it private or reveal it when the moment feels right.", + cta = "Answer now", + target = HomeActionTarget.DailyQuestion, + tone = HomeActionTone.Daily, + metric = dailyQuestion?.category?.takeIf { category -> category.isNotBlank() }?.toHomeLabel() + ) + + answerStats.private > 0 -> HomeAction( + eyebrow = "Saved privately", + title = "You have ${answerStats.private} reflection${if (answerStats.private == 1) "" else "s"} waiting.", + body = "Review what you saved and choose whether tonight is the right time to open one up.", + cta = "Review reflections", + target = HomeActionTarget.AnswerHistory, + tone = HomeActionTone.Reflection, + metric = "${answerStats.revealed} revealed" + ) + + streakCount > 0 -> HomeAction( + eyebrow = "Shared ritual", + title = "$streakCount night${if (streakCount == 1) "" else "s"} showing up.", + body = "Keep it light: answer one prompt, revisit a saved reflection, or choose a pack that fits tonight.", + cta = "Keep going", + target = HomeActionTarget.DailyQuestion, + tone = HomeActionTone.Ritual, + metric = "${answerStats.total} saved" + ) + + else -> HomeAction( + eyebrow = "Gentle start", + title = "Start with one question worth answering.", + body = "A small prompt is enough. Build the habit around attention, not pressure.", + cta = "Start tonight", + target = HomeActionTarget.DailyQuestion, + tone = HomeActionTone.Starter + ) + } + } + + private fun HomeUiState.buildSecondaryActions(primary: HomeAction): List { + val actions = mutableListOf() + + answerStats.latest?.let { latest -> + if (primary.target != HomeActionTarget.AnswerHistory) { + actions += HomeAction( + eyebrow = if (latest.isRevealed) "Revealed" else "Private", + title = "Return to your latest reflection.", + body = latest.questionText, + cta = "Open history", + target = HomeActionTarget.AnswerHistory, + tone = HomeActionTone.Reflection + ) + } + } + + categories.firstOrNull()?.let { category -> + actions += HomeAction( + eyebrow = "Suggested pack", + title = category.category.displayName.ifBlank { "Question pack" }, + body = "${category.questionCount} prompts for when you want a different doorway into the conversation.", + cta = "Open pack", + target = HomeActionTarget.QuestionPacks, + tone = HomeActionTone.Pack, + categoryId = category.category.id + ) + } + + actions += HomeAction( + eyebrow = "Tune the ritual", + title = "Adjust your space.", + body = "Manage reminders, partner state, privacy, and account details when you need to.", + cta = "Settings", + target = HomeActionTarget.Settings, + tone = HomeActionTone.Utility + ) + + return actions.take(3) + } + + private fun String.toHomeLabel(): String = + split("_", "-") + .filter { part -> part.isNotBlank() } + .joinToString(" ") { part -> part.replaceFirstChar { it.uppercaseChar() } } }