feat: update AnswerHistory, BucketList, DateMatches, Onboarding, LocalQuestionContent, WheelHistory screens

This commit is contained in:
null 2026-06-19 03:53:43 -05:00
parent 803b681d06
commit 164e0e47ca
6 changed files with 413 additions and 120 deletions

View File

@ -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."
)
}
}

View File

@ -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,

View File

@ -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)
)

View File

@ -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,17 +81,144 @@ 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()
.navigationBarsPadding()
) {
// 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
)
}
} else {
Spacer(Modifier.height(40.dp))
}
}
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
) {
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
@ -81,15 +226,11 @@ fun OnboardingScreen(
Spacer(Modifier.height(1.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(Modifier.height(80.dp))
Spacer(Modifier.height(40.dp))
Box(
modifier = Modifier
.size(96.dp)
.shadow(
elevation = 14.dp,
shape = RoundedCornerShape(24.dp),
clip = false
)
.shadow(elevation = 14.dp, shape = RoundedCornerShape(24.dp), clip = false)
.clip(RoundedCornerShape(24.dp))
) {
Image(
@ -102,9 +243,7 @@ fun OnboardingScreen(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.matchParentSize()
.alpha(0.96f)
modifier = Modifier.matchParentSize().alpha(0.96f)
)
}
Spacer(Modifier.height(28.dp))
@ -116,7 +255,7 @@ fun OnboardingScreen(
)
Spacer(Modifier.height(12.dp))
Text(
text = "Questions that bring couples closer,\none answer at a time.",
text = "Ready when you are.",
style = MaterialTheme.typography.bodyLarge,
color = AuthMuted,
textAlign = TextAlign.Center
@ -135,7 +274,7 @@ fun OnboardingScreen(
contentColor = AuthOnPrimary
)
) {
Text("Get started", style = MaterialTheme.typography.labelLarge)
Text("Create account", style = MaterialTheme.typography.labelLarge)
}
Spacer(Modifier.height(12.dp))
TextButton(onClick = { onNavigate(AppRoute.LOGIN) }) {
@ -147,6 +286,197 @@ fun OnboardingScreen(
}
}
}
}
// ── 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))
)
}
}
}

View File

@ -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

View File

@ -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) }
)
}