feat: local question content, question header, daily question illustration, iOS question/wheel views

This commit is contained in:
null 2026-06-22 13:57:09 -05:00
parent f350c91b55
commit 174e56c5a0
6 changed files with 155 additions and 75 deletions

View File

@ -6,6 +6,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -27,8 +28,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -37,20 +36,17 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
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.TextOverflow
import androidx.compose.ui.unit.dp
import app.closer.R
import app.closer.domain.model.ChoiceAnswerConfigImpl
import app.closer.domain.model.Question
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
@ -121,12 +117,11 @@ fun LocalQuestionContent(
val reducedMotion = Settings.Global.getFloat(
context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f
) == 0f
var helpExpanded by remember(question.id) { mutableStateOf(false) }
QuestionMetaRow(question = question)
QuestionHeader(
question = question,
helpExpanded = helpExpanded,
onToggleHelp = { helpExpanded = !helpExpanded },
helpExpanded = false,
onToggleHelp = {},
showHelp = false
)
QuestionAnswerInput(
question = question,
@ -205,50 +200,47 @@ private fun LocalQuestionHeader(
title: String,
subtitle: String
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
text = title,
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
@Composable
private fun QuestionMetaRow(question: Question) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
MetaPill(label = question.category.displayCategoryName())
MetaPill(label = "Depth ${question.depthLevel}")
MetaPill(label = question.type.displayQuestionType())
}
}
@Composable
private fun MetaPill(label: String) {
Surface(
shape = RoundedCornerShape(999.dp),
color = Color.White.copy(alpha = 0.72f),
shadowElevation = 0.dp
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
color = Color.White.copy(alpha = 0.82f),
shadowElevation = 5.dp
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Image(
painter = painterResource(R.drawable.illustration_daily_question),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(166.dp)
.clip(RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp))
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

View File

@ -10,6 +10,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -22,21 +23,22 @@ fun QuestionHeader(
question: Question,
helpExpanded: Boolean,
onToggleHelp: () -> Unit,
showHelp: Boolean = true,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
shape = RoundedCornerShape(26.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
containerColor = Color(0xFFFFF8FC).copy(alpha = 0.94f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 28.dp)
.padding(horizontal = 24.dp, vertical = 30.dp)
) {
Text(
text = question.text,
@ -45,19 +47,21 @@ fun QuestionHeader(
fontWeight = FontWeight.SemiBold,
lineHeight = 34.sp
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 6,
overflow = TextOverflow.Ellipsis
)
}
}
QuestionHelpExpandable(
question = question,
expanded = helpExpanded,
onToggle = onToggleHelp,
modifier = Modifier.padding(top = 8.dp)
)
if (showHelp) {
QuestionHelpExpandable(
question = question,
expanded = helpExpanded,
onToggle = onToggleHelp,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

View File

@ -11,14 +11,16 @@ struct DailyQuestionView: View {
var body: some View {
ScrollView {
VStack(spacing: CloserSpacing.xxl) {
VStack(spacing: CloserSpacing.lg) {
if isLoading {
LoadingView(message: "Loading today's question...")
.padding(.top, CloserSpacing.xxxl)
} else if let question = question {
// Question card
TodayQuestionHeroView()
.closerPadding()
VStack(spacing: CloserSpacing.lg) {
Text("Today's Question")
Text("Today's question")
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
@ -143,6 +145,35 @@ struct DailyQuestionView: View {
}
}
private struct TodayQuestionHeroView: View {
var body: some View {
VStack(alignment: .leading, spacing: CloserSpacing.md) {
Image("illustration-daily-question")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity)
.frame(height: 174)
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: CloserSpacing.xs) {
Text("One question, enough space")
.font(CloserFont.title2)
.foregroundColor(.closerText)
Text("Answer privately first, then reveal when you are both ready.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(CloserSpacing.md)
.background(Color.closerSurface)
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
.closerShadow(level: .small)
}
}
// MARK: - Question Answer
struct QuestionAnswerView: View {

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

View File

@ -58,6 +58,35 @@ struct CategoryPickerView: View {
}
}
// MARK: - Wheel Copy
private enum WheelCopy {
static let titles = [
"Let the Prompt Find You",
"Spin for Tonight's Prompt",
"Let Fate Pick",
"The Wheel Has Opinions",
"Find Our Prompt",
"Tiny Moment, Big Maybe",
]
static let subtitles = [
"Spin for tonight's prompt.",
"Let fate pick the vibe.",
"Not sure? Spin.",
"Tiny moment. Big maybe.",
"No pressure. Just spin.",
"Let the wheel choose.",
]
static let buttonLabels = ["Spin", "Let's Spin"]
static let results = [
"The wheel has spoken.",
"This one found you.",
"Fate picked this one.",
"Prompt unlocked.",
"This is the one.",
]
}
// MARK: - Spin Wheel
struct SpinWheelView: View {
@ -67,6 +96,10 @@ struct SpinWheelView: View {
@State private var selectedSlice: Int?
@State private var showResult = false
@State private var navigateToSession = false
@State private var titleIndex: Int = 0
@State private var subtitleIndex: Int = 0
@State private var buttonIndex: Int = 0
@State private var resultIndex: Int = 0
let slices: [(label: String, color: Color)] = [
("Question", .closerPrimary),
@ -81,9 +114,23 @@ struct SpinWheelView: View {
var body: some View {
VStack(spacing: CloserSpacing.xl) {
Text("Category: \(category)")
.font(CloserFont.title2)
.foregroundColor(.closerText)
VStack(spacing: CloserSpacing.xs) {
Text(WheelCopy.titles[titleIndex])
.font(CloserFont.title2)
.foregroundColor(.closerText)
.multilineTextAlignment(.center)
Text(WheelCopy.subtitles[subtitleIndex])
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
Text(category)
.font(CloserFont.caption)
.foregroundColor(.closerPrimary)
.padding(.horizontal, CloserSpacing.sm)
.padding(.vertical, 4)
.background(Color.closerPrimary.opacity(0.1))
.clipShape(Capsule())
}
.padding(.top, CloserSpacing.sm)
// Wheel
ZStack {
@ -114,8 +161,8 @@ struct SpinWheelView: View {
// Result
if let slice = selectedSlice, showResult {
VStack(spacing: CloserSpacing.sm) {
Text("You got:")
VStack(spacing: CloserSpacing.xs) {
Text(WheelCopy.results[resultIndex])
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
Text(slices[slice].label)
@ -132,7 +179,7 @@ struct SpinWheelView: View {
Button(action: spin) {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text(isSpinning ? "Spinning..." : "Spin!")
Text(isSpinning ? "Spinning" : WheelCopy.buttonLabels[buttonIndex])
}
}
.buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning))
@ -152,6 +199,11 @@ struct SpinWheelView: View {
.navigationDestination(isPresented: $navigateToSession) {
WheelSessionView(sessionId: UUID().uuidString, category: category, slice: slices[selectedSlice ?? 0].label)
}
.onAppear {
titleIndex = Int.random(in: 0..<WheelCopy.titles.count)
subtitleIndex = Int.random(in: 0..<WheelCopy.subtitles.count)
buttonIndex = Int.random(in: 0..<WheelCopy.buttonLabels.count)
}
}
private func spin() {
@ -174,6 +226,7 @@ struct SpinWheelView: View {
let index = Int(pointerAngle / sliceAngle)
selectedSlice = min(max(index, 0), slices.count - 1)
resultIndex = Int.random(in: 0..<WheelCopy.results.count)
showResult = true
isSpinning = false
}