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 299b2eb8ca
commit 324b051834
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.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -37,20 +36,17 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.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.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.closer.R
import app.closer.domain.model.ChoiceAnswerConfigImpl import app.closer.domain.model.ChoiceAnswerConfigImpl
import app.closer.domain.model.Question import app.closer.domain.model.Question
import app.closer.domain.model.ThisOrThatAnswerConfigImpl import app.closer.domain.model.ThisOrThatAnswerConfigImpl
@ -121,12 +117,11 @@ fun LocalQuestionContent(
val reducedMotion = Settings.Global.getFloat( val reducedMotion = Settings.Global.getFloat(
context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f
) == 0f ) == 0f
var helpExpanded by remember(question.id) { mutableStateOf(false) }
QuestionMetaRow(question = question)
QuestionHeader( QuestionHeader(
question = question, question = question,
helpExpanded = helpExpanded, helpExpanded = false,
onToggleHelp = { helpExpanded = !helpExpanded }, onToggleHelp = {},
showHelp = false
) )
QuestionAnswerInput( QuestionAnswerInput(
question = question, question = question,
@ -205,50 +200,47 @@ private fun LocalQuestionHeader(
title: String, title: String,
subtitle: 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( Surface(
shape = RoundedCornerShape(999.dp), modifier = Modifier.fillMaxWidth(),
color = Color.White.copy(alpha = 0.72f), shape = RoundedCornerShape(28.dp),
shadowElevation = 0.dp color = Color.White.copy(alpha = 0.82f),
shadowElevation = 5.dp
) { ) {
Text( Column(
text = label, modifier = Modifier.fillMaxWidth(),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(14.dp)
style = MaterialTheme.typography.labelMedium, ) {
color = MaterialTheme.colorScheme.onSurface, Image(
maxLines = 1, painter = painterResource(R.drawable.illustration_daily_question),
overflow = TextOverflow.Ellipsis 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -22,21 +23,22 @@ fun QuestionHeader(
question: Question, question: Question,
helpExpanded: Boolean, helpExpanded: Boolean,
onToggleHelp: () -> Unit, onToggleHelp: () -> Unit,
showHelp: Boolean = true,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier.fillMaxWidth()) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(26.dp),
colors = CardDefaults.cardColors( 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 28.dp) .padding(horizontal = 24.dp, vertical = 30.dp)
) { ) {
Text( Text(
text = question.text, text = question.text,
@ -45,19 +47,21 @@ fun QuestionHeader(
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 34.sp lineHeight = 34.sp
), ),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
maxLines = 6, maxLines = 6,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
} }
QuestionHelpExpandable( if (showHelp) {
question = question, QuestionHelpExpandable(
expanded = helpExpanded, question = question,
onToggle = onToggleHelp, expanded = helpExpanded,
modifier = Modifier.padding(top = 8.dp) 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 { var body: some View {
ScrollView { ScrollView {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.lg) {
if isLoading { if isLoading {
LoadingView(message: "Loading today's question...") LoadingView(message: "Loading today's question...")
.padding(.top, CloserSpacing.xxxl) .padding(.top, CloserSpacing.xxxl)
} else if let question = question { } else if let question = question {
// Question card TodayQuestionHeroView()
.closerPadding()
VStack(spacing: CloserSpacing.lg) { VStack(spacing: CloserSpacing.lg) {
Text("Today's Question") Text("Today's question")
.font(CloserFont.subheadline) .font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary) .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 // MARK: - Question Answer
struct QuestionAnswerView: View { 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 // MARK: - Spin Wheel
struct SpinWheelView: View { struct SpinWheelView: View {
@ -67,6 +96,10 @@ struct SpinWheelView: View {
@State private var selectedSlice: Int? @State private var selectedSlice: Int?
@State private var showResult = false @State private var showResult = false
@State private var navigateToSession = 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)] = [ let slices: [(label: String, color: Color)] = [
("Question", .closerPrimary), ("Question", .closerPrimary),
@ -81,9 +114,23 @@ struct SpinWheelView: View {
var body: some View { var body: some View {
VStack(spacing: CloserSpacing.xl) { VStack(spacing: CloserSpacing.xl) {
Text("Category: \(category)") VStack(spacing: CloserSpacing.xs) {
.font(CloserFont.title2) Text(WheelCopy.titles[titleIndex])
.foregroundColor(.closerText) .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 // Wheel
ZStack { ZStack {
@ -114,8 +161,8 @@ struct SpinWheelView: View {
// Result // Result
if let slice = selectedSlice, showResult { if let slice = selectedSlice, showResult {
VStack(spacing: CloserSpacing.sm) { VStack(spacing: CloserSpacing.xs) {
Text("You got:") Text(WheelCopy.results[resultIndex])
.font(CloserFont.subheadline) .font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
Text(slices[slice].label) Text(slices[slice].label)
@ -132,7 +179,7 @@ struct SpinWheelView: View {
Button(action: spin) { Button(action: spin) {
HStack { HStack {
Image(systemName: "arrow.triangle.2.circlepath") Image(systemName: "arrow.triangle.2.circlepath")
Text(isSpinning ? "Spinning..." : "Spin!") Text(isSpinning ? "Spinning" : WheelCopy.buttonLabels[buttonIndex])
} }
} }
.buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning)) .buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning))
@ -152,6 +199,11 @@ struct SpinWheelView: View {
.navigationDestination(isPresented: $navigateToSession) { .navigationDestination(isPresented: $navigateToSession) {
WheelSessionView(sessionId: UUID().uuidString, category: category, slice: slices[selectedSlice ?? 0].label) 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() { private func spin() {
@ -174,6 +226,7 @@ struct SpinWheelView: View {
let index = Int(pointerAngle / sliceAngle) let index = Int(pointerAngle / sliceAngle)
selectedSlice = min(max(index, 0), slices.count - 1) selectedSlice = min(max(index, 0), slices.count - 1)
resultIndex = Int.random(in: 0..<WheelCopy.results.count)
showResult = true showResult = true
isSpinning = false isSpinning = false
} }