feat: local question content, question header, daily question illustration, iOS question/wheel views
This commit is contained in:
parent
f350c91b55
commit
174e56c5a0
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue