fix(answers): update answer flow screens, navigation, and question input component
This commit is contained in:
parent
56f2d8c045
commit
0f73c656d8
|
|
@ -74,9 +74,20 @@ fun AppNavigation(
|
|||
val shellTitle = currentRoute
|
||||
?.takeIf { it in shellBackRoutes }
|
||||
?.let(AppRoute::titleFor)
|
||||
val navigateBackOrHome: () -> Unit = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(AppRoute.HOME) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
val navigateRoute: (String) -> Unit = { route ->
|
||||
if (route == "back") {
|
||||
navController.popBackStack()
|
||||
navigateBackOrHome()
|
||||
} else {
|
||||
navController.navigate(route)
|
||||
}
|
||||
|
|
@ -94,7 +105,7 @@ fun AppNavigation(
|
|||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
IconButton(onClick = navigateBackOrHome) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
|
|
@ -200,7 +211,7 @@ fun AppNavigation(
|
|||
previousQuestionId = it.arguments?.getString("prevId"),
|
||||
nextQuestionId = it.arguments?.getString("nextId"),
|
||||
onNavigate = navigateRoute,
|
||||
onBack = { navController.popBackStack() }
|
||||
onBack = navigateBackOrHome
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,15 +13,21 @@ import androidx.compose.foundation.layout.safeDrawingPadding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
|
@ -58,12 +64,43 @@ private fun AnswerHistoryContent(
|
|||
onDailyQuestion: () -> Unit,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
|
||||
|
||||
pendingDelete?.let { answer ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingDelete = null },
|
||||
title = { Text("Remove this answer?") },
|
||||
text = {
|
||||
Text("This removes the saved reflection from this device. The prompt itself will stay available.")
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onDelete(answer.questionId)
|
||||
pendingDelete = null
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF8D2D35),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Remove")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { pendingDelete = null }) {
|
||||
Text("Keep")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
|
||||
listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
|
|
@ -109,7 +146,7 @@ private fun AnswerHistoryContent(
|
|||
AnswerHistoryCard(
|
||||
answer = answer,
|
||||
onClick = { onAnswerSelected(answer) },
|
||||
onDelete = { onDelete(answer.questionId) }
|
||||
onDelete = { pendingDelete = answer }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +165,7 @@ private fun AnswerHistoryCard(
|
|||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.86f)),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -154,12 +191,22 @@ private fun AnswerHistoryCard(
|
|||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onDelete,
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Remove answer")
|
||||
Text(
|
||||
text = if (answer.isRevealed) "Opened" else "Private",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color(0xFF6B4A86),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
TextButton(onClick = onDelete) {
|
||||
Text(
|
||||
text = "Remove",
|
||||
color = Color(0xFF8D2D35)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ private fun AnswerRevealContent(
|
|||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
|
||||
listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
|
|
@ -89,16 +89,7 @@ private fun AnswerRevealContent(
|
|||
.padding(horizontal = 20.dp, vertical = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Reveal together",
|
||||
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF27211F)
|
||||
)
|
||||
Text(
|
||||
text = "Open a saved answer when you are ready to look at it together.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF4E4642)
|
||||
)
|
||||
RevealHeader()
|
||||
|
||||
when {
|
||||
state.isLoading -> RevealMessageCard {
|
||||
|
|
@ -151,15 +142,15 @@ private fun NoAnswerState(
|
|||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
RevealPill("No answer yet")
|
||||
Text(
|
||||
text = question?.text ?: "Question $questionId is ready when you are.",
|
||||
text = question?.text ?: "This prompt is ready when you are.",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color(0xFF27211F),
|
||||
color = Color(0xFF261D2E),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "Answer this prompt first, then come back when you are ready to reveal it.",
|
||||
text = "Answer privately first. Reveal can wait until there is something worth opening together.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642)
|
||||
color = Color(0xFF5A5060)
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
|
|
@ -167,9 +158,9 @@ private fun NoAnswerState(
|
|||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF271236)
|
||||
)
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF24122F)
|
||||
)
|
||||
) {
|
||||
Text("Answer")
|
||||
}
|
||||
|
|
@ -178,7 +169,7 @@ private fun NoAnswerState(
|
|||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Text("Home")
|
||||
Text("Not now")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -198,13 +189,14 @@ private fun ReadyToRevealState(
|
|||
Text(
|
||||
text = question?.text ?: answer.questionText,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color(0xFF27211F),
|
||||
color = Color(0xFF261D2E),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
AnswerPreview(answer = answer, revealed = false)
|
||||
Text(
|
||||
text = "Your answer is private for now. Reveal it when the moment feels right.",
|
||||
text = "No rush. Reveal this only when you want the conversation to open.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642)
|
||||
color = Color(0xFF5A5060)
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
|
|
@ -212,18 +204,18 @@ private fun ReadyToRevealState(
|
|||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF271236)
|
||||
)
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF24122F)
|
||||
)
|
||||
) {
|
||||
Text("Reveal")
|
||||
Text("Reveal answer")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onHistory,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Text("History")
|
||||
Text("Saved answers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,29 +239,21 @@ private fun RevealedState(
|
|||
Text(
|
||||
text = question?.text ?: answer.questionText,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color(0xFF27211F),
|
||||
color = Color(0xFF261D2E),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color(0xFFF8F0FF)
|
||||
) {
|
||||
Text(
|
||||
text = answer.revealSummary(),
|
||||
modifier = Modifier.padding(18.dp),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF3E3734)
|
||||
)
|
||||
}
|
||||
AnswerPreview(answer = answer, revealed = true)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
onClick = onHistory,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF81B29A))
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF24122F)
|
||||
)
|
||||
) {
|
||||
Text("History")
|
||||
Text("Saved answers")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onHome,
|
||||
|
|
@ -288,7 +272,7 @@ private fun RevealMessageCard(content: @Composable () -> Unit) {
|
|||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.86f)),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.padding(20.dp)) {
|
||||
|
|
@ -297,6 +281,51 @@ private fun RevealMessageCard(content: @Composable () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevealHeader() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = "Reveal together",
|
||||
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF261D2E)
|
||||
)
|
||||
Text(
|
||||
text = "A saved answer can stay private, become a shared reflection, or simply wait for the right moment.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF5A5060)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnswerPreview(
|
||||
answer: LocalAnswer,
|
||||
revealed: Boolean
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (revealed) Color(0xFFF4E8FF) else Color(0xFFFFF8FC)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (revealed) "Opened answer" else "Private preview",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color(0xFF56306F),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = if (revealed) answer.revealSummary() else answer.privatePreview(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF3E3346)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevealPill(label: String) {
|
||||
Surface(
|
||||
|
|
@ -323,6 +352,16 @@ fun LocalAnswer.revealSummary(): String {
|
|||
}
|
||||
}
|
||||
|
||||
private fun LocalAnswer.privatePreview(): String {
|
||||
return when (answerType) {
|
||||
"written" -> writtenText?.takeIf { it.isNotBlank() }?.let { "Your written answer is saved." }
|
||||
?: "Your answer is saved."
|
||||
"scale" -> "Your scale answer is saved."
|
||||
"single_choice", "multi_choice", "this_or_that" -> "Your choice is saved."
|
||||
else -> "Your answer is saved."
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnswerRevealScreenPreview() {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ fun LocalQuestionContent(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val background = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFFFFFBFA), Color(0xFFF3F7F1), Color(0xFFEAF0F4)),
|
||||
colors = listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
|
|
@ -135,7 +135,7 @@ fun LocalQuestionContent(
|
|||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
contentColor = Color(0xFF271236)
|
||||
contentColor = Color(0xFF24122F)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -231,44 +231,46 @@ private fun SubmittedAnswerCard(
|
|||
question: Question,
|
||||
state: LocalQuestionUiState
|
||||
) {
|
||||
Card(
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFFFFF).copy(alpha = 0.86f)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = Color.White.copy(alpha = 0.78f),
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(15.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF81B29A).copy(alpha = 0.22f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "OK",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF2F5D45),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFB98AF4).copy(alpha = 0.18f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Saved privately",
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color(0xFF27211F),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
text = "OK",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF56306F),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(
|
||||
text = "Private answer saved",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color(0xFF261D2E),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = answerSummary(question, state),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF5A5060),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = answerSummary(question, state),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ fun QuestionThreadScreen(
|
|||
LocalQuestionContent(
|
||||
state = state,
|
||||
title = "Answer with care",
|
||||
subtitle = "Take a moment for your own answer. When you are ready, move to the next prompt or revisit what you have saved.",
|
||||
primaryRouteLabel = nextQuestionId?.let { "Next" } ?: "History",
|
||||
subtitle = "Save your answer privately first. From there, choose whether to keep moving or revisit what you have opened.",
|
||||
primaryRouteLabel = nextQuestionId?.let { "Next prompt" } ?: "Saved answers",
|
||||
onPrimaryRoute = {
|
||||
if (nextQuestionId != null) {
|
||||
onNavigate(
|
||||
|
|
@ -73,8 +73,8 @@ fun QuestionThreadScreenPreview() {
|
|||
)
|
||||
),
|
||||
title = "Answer with care",
|
||||
subtitle = "Take a moment for your own answer.",
|
||||
primaryRouteLabel = "History",
|
||||
subtitle = "Save your answer privately first.",
|
||||
primaryRouteLabel = "Saved answers",
|
||||
onPrimaryRoute = {},
|
||||
onSecondaryRoute = {},
|
||||
secondaryRouteLabel = "Back",
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ fun QuestionAnswerInput(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = if (isSubmitting) "Saving…" else "Submit answer",
|
||||
text = if (isSubmitting) "Saving…" else "Save privately",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue