From 164e0e47ca18fabcd69f07ddf0b2c0cd2383ac8d Mon Sep 17 00:00:00 2001 From: null Date: Fri, 19 Jun 2026 03:53:43 -0500 Subject: [PATCH] feat: update AnswerHistory, BucketList, DateMatches, Onboarding, LocalQuestionContent, WheelHistory screens --- .../closer/ui/answers/AnswerHistoryScreen.kt | 10 +- .../app/closer/ui/dates/BucketListScreen.kt | 43 +- .../app/closer/ui/dates/DateMatchesScreen.kt | 6 +- .../closer/ui/onboarding/OnboardingScreen.kt | 464 +++++++++++++++--- .../ui/questions/LocalQuestionContent.kt | 4 +- .../app/closer/ui/wheel/WheelHistoryScreen.kt | 6 +- 6 files changed, 413 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt index af1a9417..715d2584 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerHistoryScreen.kt @@ -159,9 +159,9 @@ private fun AnswerHistoryContent( if (state.answers.isEmpty()) { item { EmptyState( - title = "No answers saved yet", - body = "Answer a daily question or choose a prompt from a pack, and it will appear here.", - actionLabel = "Daily question", + title = "Nothing here yet", + body = "Answer today's question or pick a prompt from a pack. Every answer you save will wait for you here.", + actionLabel = "Today's question", onAction = onDailyQuestion ) } @@ -178,8 +178,8 @@ private fun AnswerHistoryContent( if (visibleAnswers.isEmpty()) { item { EmptyState( - title = "Nothing in this view", - body = "Switch filters to see the rest of your saved answers." + title = "Nothing in this filter", + body = "Your answers are here — just in the other tab. Switch between Private and Revealed to see them." ) } } diff --git a/app/src/main/java/app/closer/ui/dates/BucketListScreen.kt b/app/src/main/java/app/closer/ui/dates/BucketListScreen.kt index 4a64f7d8..e4f2cf24 100644 --- a/app/src/main/java/app/closer/ui/dates/BucketListScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/BucketListScreen.kt @@ -256,7 +256,9 @@ private fun BucketListItems( modifier: Modifier = Modifier ) { if (items.isEmpty()) { - EmptyState( + app.closer.ui.components.EmptyState( + title = "Your shared list is empty", + body = "Add something you've been dreaming about doing together — big or small. Tap + to start.", modifier = Modifier.padding(top = 80.dp) ) return @@ -370,45 +372,6 @@ private fun CategoryBadge( } } -@Composable -private fun EmptyState( - modifier: Modifier = Modifier -) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Surface( - shape = RoundedCornerShape(999.dp), - color = Color(0xFFB98AF4).copy(alpha = 0.16f), - modifier = Modifier.size(64.dp) - ) { - Icon( - imageVector = Icons.Filled.Info, - contentDescription = null, - tint = Color(0xFF56306F), - modifier = Modifier.size(32.dp) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "No bucket list items yet", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - Text( - text = "Tap the + button to add your first dream date!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = androidx.compose.ui.text.style.TextAlign.Center - ) - } -} - @Composable private fun AddItemDialog( title: String, diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt index 9b608867..a92ae9fb 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt @@ -124,11 +124,11 @@ private fun DateMatchesContent( state.mutualMatches.isEmpty() && state.maybeMatches.isEmpty() -> item { EmptyState( - title = "No matches yet", + title = "No mutual matches yet", body = buildString { - append("Start swiping on date ideas. When you and ") + append("Swipe through date ideas — when you and ") append(state.partnerName ?: "your partner") - append(" both love the same idea, it will appear here.") + append(" both love the same one, it shows up here as a match.") }, modifier = Modifier.padding(top = 80.dp) ) diff --git a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt index 6174f04b..15c344f9 100644 --- a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt @@ -1,35 +1,49 @@ package app.closer.ui.onboarding -import androidx.compose.foundation.background +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -41,6 +55,10 @@ import app.closer.ui.auth.AuthMuted import app.closer.ui.auth.AuthOnPrimary import app.closer.ui.auth.AuthPrimary import app.closer.ui.auth.AuthPrimaryDeep +import app.closer.ui.theme.CloserPalette +import kotlinx.coroutines.launch + +private const val CTA_PAGE = 3 @Composable fun OnboardingScreen( @@ -63,90 +81,402 @@ fun OnboardingScreen( ) { if (state.isCheckingAuth) { CircularProgressIndicator( - modifier = Modifier - .size(36.dp) - .align(Alignment.Center), + modifier = Modifier.size(36.dp).align(Alignment.Center), color = AuthPrimary, strokeWidth = 3.dp ) } else { + val pagerState = rememberPagerState(pageCount = { CTA_PAGE + 1 }) + val scope = rememberCoroutineScope() + Column( modifier = Modifier .fillMaxSize() .safeDrawingPadding() - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + .navigationBarsPadding() ) { - Spacer(Modifier.height(1.dp)) - - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(Modifier.height(80.dp)) - Box( - modifier = Modifier - .size(96.dp) - .shadow( - elevation = 14.dp, - shape = RoundedCornerShape(24.dp), - clip = false + // Skip button — visible on value slides only + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp, top = 8.dp), + contentAlignment = Alignment.CenterEnd + ) { + if (pagerState.currentPage < CTA_PAGE) { + TextButton( + onClick = { + scope.launch { pagerState.animateScrollToPage(CTA_PAGE) } + } + ) { + Text( + "Skip", + style = MaterialTheme.typography.bodyMedium, + color = AuthMuted ) - .clip(RoundedCornerShape(24.dp)) - ) { - Image( - painter = painterResource(R.drawable.ic_launcher_background), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.matchParentSize() - ) - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .matchParentSize() - .alpha(0.96f) - ) + } + } else { + Spacer(Modifier.height(40.dp)) } - Spacer(Modifier.height(28.dp)) - Text( - text = "Closer", - style = MaterialTheme.typography.displayMedium, - color = AuthInk, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(12.dp)) - Text( - text = "Questions that bring couples closer,\none answer at a time.", - style = MaterialTheme.typography.bodyLarge, - color = AuthMuted, - textAlign = TextAlign.Center - ) } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(bottom = 48.dp) - ) { - Button( - onClick = { onNavigate(AppRoute.SIGN_UP) }, - modifier = Modifier.fillMaxWidth().height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = AuthPrimary, - contentColor = AuthOnPrimary + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f) + ) { page -> + when (page) { + 0 -> ValueSlide( + visual = { AnswerPreviewVisual() }, + headline = "Answer honestly", + body = "Every day, both of you answer the same question — separately, on your own devices." ) + 1 -> ValueSlide( + visual = { NoPeekingVisual() }, + headline = "No peeking", + body = "Answers stay hidden until you've both finished. Then they reveal side by side." + ) + 2 -> ValueSlide( + visual = { GrowerVisual() }, + headline = "Grow closer", + body = "Every answer opens a real conversation. Not a quiz — a moment." + ) + else -> CtaSlide(onNavigate = onNavigate) + } + } + + // Page dots + Next button + if (pagerState.currentPage < CTA_PAGE) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text("Get started", style = MaterialTheme.typography.labelLarge) - } - Spacer(Modifier.height(12.dp)) - TextButton(onClick = { onNavigate(AppRoute.LOGIN) }) { - Text( - "I already have an account", - style = MaterialTheme.typography.bodyMedium, - color = AuthPrimaryDeep - ) + PageDots(current = pagerState.currentPage, total = CTA_PAGE) + Button( + onClick = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = AuthPrimary, + contentColor = AuthOnPrimary + ) + ) { + Text( + if (pagerState.currentPage == CTA_PAGE - 1) "Get started" else "Next", + style = MaterialTheme.typography.labelLarge + ) + } } + } else { + Spacer(Modifier.height(20.dp)) } } } } } + +// ── Slides ──────────────────────────────────────────────────────────────────── + +@Composable +private fun ValueSlide( + visual: @Composable () -> Unit, + headline: String, + body: String +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Spacer(Modifier.height(16.dp)) + visual() + Spacer(Modifier.height(36.dp)) + Text( + text = headline, + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), + color = AuthInk, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(12.dp)) + Text( + text = body, + style = MaterialTheme.typography.bodyLarge, + color = AuthMuted, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun CtaSlide(onNavigate: (String) -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Spacer(Modifier.height(1.dp)) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(Modifier.height(40.dp)) + Box( + modifier = Modifier + .size(96.dp) + .shadow(elevation = 14.dp, shape = RoundedCornerShape(24.dp), clip = false) + .clip(RoundedCornerShape(24.dp)) + ) { + Image( + painter = painterResource(R.drawable.ic_launcher_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize() + ) + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.matchParentSize().alpha(0.96f) + ) + } + Spacer(Modifier.height(28.dp)) + Text( + text = "Closer", + style = MaterialTheme.typography.displayMedium, + color = AuthInk, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(12.dp)) + Text( + text = "Ready when you are.", + style = MaterialTheme.typography.bodyLarge, + color = AuthMuted, + textAlign = TextAlign.Center + ) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(bottom = 48.dp) + ) { + Button( + onClick = { onNavigate(AppRoute.SIGN_UP) }, + modifier = Modifier.fillMaxWidth().height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = AuthPrimary, + contentColor = AuthOnPrimary + ) + ) { + Text("Create account", style = MaterialTheme.typography.labelLarge) + } + Spacer(Modifier.height(12.dp)) + TextButton(onClick = { onNavigate(AppRoute.LOGIN) }) { + Text( + "I already have an account", + style = MaterialTheme.typography.bodyMedium, + color = AuthPrimaryDeep + ) + } + } + } +} + +// ── Visuals ─────────────────────────────────────────────────────────────────── + +@Composable +private fun AnswerPreviewVisual() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Mock question card + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "What's something you want more of in your relationship?", + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium), + color = Color(0xFF1A1A2E), + textAlign = TextAlign.Center + ) + // Two mock answer bubbles + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + MockAnswerBubble("Quality time", selected = true, modifier = Modifier.weight(1f)) + MockAnswerBubble("Adventure", selected = false, modifier = Modifier.weight(1f)) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + MockAnswerBubble("Deep talks", selected = false, modifier = Modifier.weight(1f)) + MockAnswerBubble("Playfulness", selected = false, modifier = Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun MockAnswerBubble(label: String, selected: Boolean, modifier: Modifier = Modifier) { + val bg by animateColorAsState( + if (selected) CloserPalette.PurpleDeep else Color(0xFFF4F0FF), + animationSpec = tween(200) + ) + val fg by animateColorAsState( + if (selected) Color.White else Color(0xFF56306F), + animationSpec = tween(200) + ) + Surface( + modifier = modifier, + shape = RoundedCornerShape(12.dp), + color = bg + ) { + Text( + text = label, + modifier = Modifier.padding(vertical = 10.dp, horizontal = 6.dp), + style = MaterialTheme.typography.labelMedium, + color = fg, + textAlign = TextAlign.Center, + maxLines = 1 + ) + } +} + +@Composable +private fun NoPeekingVisual() { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RevealCard( + label = "You", + preview = "Quality time", + revealed = false, + color = CloserPalette.PurpleSoft + ) + Text("→", style = MaterialTheme.typography.headlineLarge, color = AuthMuted) + RevealCard( + label = "Partner", + preview = "Deep talks", + revealed = false, + color = CloserPalette.PinkMist + ) + } + Spacer(Modifier.height(8.dp)) + Surface( + shape = RoundedCornerShape(12.dp), + color = CloserPalette.PurpleDeep.copy(alpha = 0.1f) + ) { + Text( + text = "Sealed until you both answer", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.labelMedium, + color = CloserPalette.PurpleDeep + ) + } +} + +@Composable +private fun RevealCard(label: String, preview: String, revealed: Boolean, color: Color) { + Card( + modifier = Modifier.width(120.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = color), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = CloserPalette.PurpleDeep.copy(alpha = 0.7f) + ) + if (revealed) { + Text( + text = preview, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = CloserPalette.PurpleDeep, + textAlign = TextAlign.Center + ) + } else { + // Blurred / hidden lines + repeat(3) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(RoundedCornerShape(8.dp)) + .background(CloserPalette.PurpleDeep.copy(alpha = 0.15f)) + ) + } + } + } + } +} + +@Composable +private fun GrowerVisual() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + GrowthPill("Daily question", CloserPalette.PurpleSoft, CloserPalette.PurpleDeep) + GrowthPill("Games", CloserPalette.PinkSoft, CloserPalette.Romantic) + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + GrowthPill("Desire Sync", CloserPalette.PinkMist, CloserPalette.Romantic) + GrowthPill("Memory Lane", CloserPalette.PurpleGlow, CloserPalette.PurpleDeep) + } + GrowthPill("Question threads", Color(0xFFE8F5E9), Color(0xFF2E7D32)) + } +} + +@Composable +private fun GrowthPill(label: String, bg: Color, fg: Color) { + Surface(shape = RoundedCornerShape(50.dp), color = bg) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Medium), + color = fg + ) + } +} + +// ── Page dots ───────────────────────────────────────────────────────────────── + +@Composable +private fun PageDots(current: Int, total: Int) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { + repeat(total) { i -> + val active = i == current + Box( + modifier = Modifier + .size(if (active) 20.dp else 6.dp, 6.dp) + .clip(CircleShape) + .background(if (active) AuthPrimary else AuthMuted.copy(alpha = 0.3f)) + ) + } + } +} diff --git a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt index 0b550cbc..57967a67 100644 --- a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt +++ b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt @@ -93,8 +93,8 @@ fun LocalQuestionContent( onRetry = onRefresh ) state.question == null -> EmptyState( - title = "This question is not available", - body = "Choose another prompt and keep the conversation moving gently." + title = "This one's not available", + body = "Try a different prompt — there are plenty more in your packs." ) else -> { val question = state.question diff --git a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt index 1b21393d..40e38109 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt @@ -113,9 +113,9 @@ fun WheelHistoryScreen( } state.sessions.isEmpty() -> item { EmptyState( - title = "No sessions yet", - body = "Completed games will appear here.", - actionLabel = "Play now", + title = "No games played yet", + body = "Finish a round of This or That, How Well, Desire Sync, or Spin the Wheel — your results will show up here so you can revisit them together.", + actionLabel = "Play a game", onAction = { onNavigate(AppRoute.PLAY) } ) }