feat: replace PlaceholderScreen with FinishedEmptyStateScreen across PartnerHome, EmailInvite, QuestionComposer, Subscription
This commit is contained in:
parent
8b394ab40f
commit
15c1fbdda0
|
|
@ -0,0 +1,188 @@
|
|||
package app.closer.ui.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
import app.closer.ui.theme.closerBackgroundBrush
|
||||
import app.closer.ui.theme.closerElevatedCardColor
|
||||
|
||||
data class FinishedEmptyStateAction(
|
||||
val label: String,
|
||||
val route: String
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun FinishedEmptyStateScreen(
|
||||
eyebrow: String,
|
||||
title: String,
|
||||
body: String,
|
||||
glyphCategoryId: String,
|
||||
primaryAction: FinishedEmptyStateAction,
|
||||
onNavigate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
secondaryAction: FinishedEmptyStateAction? = null,
|
||||
accent: Color = CloserPalette.PurpleDeep,
|
||||
details: List<String> = emptyList()
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(closerBackgroundBrush())
|
||||
.safeDrawingPadding()
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 22.dp, vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = 560.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = closerElevatedCardColor(),
|
||||
border = BorderStroke(1.dp, accent.copy(alpha = 0.12f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 6.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CategoryGlyph(
|
||||
categoryId = glyphCategoryId,
|
||||
size = 56.dp,
|
||||
iconSize = 27.dp
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = accent.copy(alpha = 0.10f)
|
||||
) {
|
||||
Text(
|
||||
text = eyebrow,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = accent,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = body,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (details.isNotEmpty()) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
details.forEach { detail ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(top = 7.dp)
|
||||
.size(8.dp),
|
||||
shape = CircleShape,
|
||||
color = accent.copy(alpha = 0.34f)
|
||||
) {}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = detail,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Button(
|
||||
onClick = { onNavigate(primaryAction.route) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accent,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = primaryAction.label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
secondaryAction?.let { action ->
|
||||
OutlinedButton(
|
||||
onClick = { onNavigate(action.route) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = action.label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,11 +11,9 @@ 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.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -31,6 +29,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.closer.core.navigation.AppRoute
|
||||
import app.closer.domain.usecase.GameSessionManager
|
||||
import app.closer.ui.components.CategoryGlyph
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -127,29 +126,11 @@ fun WaitingForPartnerScreen(
|
|||
)
|
||||
}
|
||||
else -> {
|
||||
// Game type icon/symbol
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.size(80.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = when (state.gameType) {
|
||||
"wheel" -> "🎡"
|
||||
"this_or_that" -> "❓"
|
||||
"how_well" -> "🧠"
|
||||
"desire_sync" -> "❤️"
|
||||
else -> "🎮"
|
||||
},
|
||||
style = MaterialTheme.typography.displayMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
CategoryGlyph(
|
||||
categoryId = gameTypeGlyphKey(state.gameType),
|
||||
size = 80.dp,
|
||||
iconSize = 38.dp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Waiting for ${state.partnerName}",
|
||||
|
|
@ -185,3 +166,11 @@ private fun gameTypeLabel(gameType: String): String = when (gameType) {
|
|||
"desire_sync" -> "Desire Sync"
|
||||
else -> gameType
|
||||
}
|
||||
|
||||
private fun gameTypeGlyphKey(gameType: String): String = when (gameType) {
|
||||
"wheel" -> "play"
|
||||
"this_or_that" -> "question"
|
||||
"how_well" -> "predict"
|
||||
"desire_sync" -> "sex_and_desire"
|
||||
else -> "play"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
package app.closer.ui.home
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.closer.core.navigation.AppRoute
|
||||
import app.closer.ui.components.PlaceholderAction
|
||||
import app.closer.ui.components.PlaceholderScreen
|
||||
import app.closer.ui.components.FinishedEmptyStateAction
|
||||
import app.closer.ui.components.FinishedEmptyStateScreen
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
|
||||
@Composable
|
||||
fun PartnerHomeScreen(
|
||||
onNavigate: (String) -> Unit = {}
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "A shared pulse",
|
||||
section = "Home",
|
||||
description = "A partner-aware view for pairing status, recent rituals, and the next shared prompt.",
|
||||
route = AppRoute.PARTNER_HOME,
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFB98AF4),
|
||||
primaryAction = PlaceholderAction("Invite partner", AppRoute.CREATE_INVITE),
|
||||
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
|
||||
chips = listOf("Partner state", "Pairing bridge", "Shared rhythm"),
|
||||
FinishedEmptyStateScreen(
|
||||
eyebrow = "Partner space",
|
||||
title = "Connect your shared home",
|
||||
body = "Invite your partner to unlock shared prompts, paired games, and a relationship rhythm built for both of you.",
|
||||
glyphCategoryId = "people",
|
||||
primaryAction = FinishedEmptyStateAction("Invite partner", AppRoute.CREATE_INVITE),
|
||||
secondaryAction = FinishedEmptyStateAction("Back home", AppRoute.HOME),
|
||||
accent = CloserPalette.PurpleDeep,
|
||||
details = listOf(
|
||||
"Know whether you are connected at a glance",
|
||||
"Keep shared activity separate from private reflections",
|
||||
"Return to the couple space without extra searching"
|
||||
)
|
||||
"Pair once, then share prompts without repeating setup.",
|
||||
"Keep shared activity distinct from private reflections.",
|
||||
"Return to the couple space when both of you are ready."
|
||||
),
|
||||
onNavigate = onNavigate
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
package app.closer.ui.pairing
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.closer.core.navigation.AppRoute
|
||||
import app.closer.ui.components.PlaceholderAction
|
||||
import app.closer.ui.components.PlaceholderScreen
|
||||
import app.closer.ui.components.FinishedEmptyStateAction
|
||||
import app.closer.ui.components.FinishedEmptyStateScreen
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
|
||||
@Composable
|
||||
fun EmailInviteScreen(
|
||||
onNavigate: (String) -> Unit = {}
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Send the thread",
|
||||
section = "Pairing",
|
||||
description = "Invite your partner with a clear message and a simple code.",
|
||||
route = AppRoute.EMAIL_INVITE,
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFB98AF4),
|
||||
primaryAction = PlaceholderAction("Confirm code", AppRoute.inviteConfirm("ABC123")),
|
||||
secondaryAction = PlaceholderAction("Create invite", AppRoute.CREATE_INVITE),
|
||||
chips = listOf("Email", "Code ABC123", "Invite"),
|
||||
FinishedEmptyStateScreen(
|
||||
eyebrow = "Pairing",
|
||||
title = "Send an invite that is easy to follow",
|
||||
body = "Create an invite code first, then share it with your partner in the channel you both already use.",
|
||||
glyphCategoryId = "communication",
|
||||
primaryAction = FinishedEmptyStateAction("Create invite", AppRoute.CREATE_INVITE),
|
||||
secondaryAction = FinishedEmptyStateAction("Enter a code", AppRoute.ACCEPT_INVITE),
|
||||
accent = CloserPalette.PurpleDeep,
|
||||
details = listOf(
|
||||
"Keep the message short and easy to understand",
|
||||
"Make the code clear before it is sent",
|
||||
"Give your partner one simple next step"
|
||||
)
|
||||
"Use one code instead of a temporary sample invite.",
|
||||
"Keep the message short enough to send anywhere.",
|
||||
"Give your partner a single next step."
|
||||
),
|
||||
onNavigate = onNavigate
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
package app.closer.ui.questions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.closer.core.navigation.AppRoute
|
||||
import app.closer.ui.components.PlaceholderAction
|
||||
import app.closer.ui.components.PlaceholderScreen
|
||||
import app.closer.ui.components.FinishedEmptyStateAction
|
||||
import app.closer.ui.components.FinishedEmptyStateScreen
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
|
||||
@Composable
|
||||
fun QuestionComposerScreen(
|
||||
onNavigate: (String) -> Unit = {}
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Ask it cleanly",
|
||||
section = "Questions",
|
||||
description = "Shape your own prompt with a tone that feels generous and clear.",
|
||||
route = AppRoute.QUESTION_COMPOSER,
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFB98AF4),
|
||||
primaryAction = PlaceholderAction("Open thread", AppRoute.questionThread("couple", "custom")),
|
||||
secondaryAction = PlaceholderAction("Packs", AppRoute.QUESTION_PACKS),
|
||||
chips = listOf("Custom prompt", "Tone aware", "Save"),
|
||||
FinishedEmptyStateScreen(
|
||||
eyebrow = "Questions",
|
||||
title = "Create a question with care",
|
||||
body = "Custom prompts are coming soon. For now, start from a pack so the tone stays clear, generous, and easy to answer.",
|
||||
glyphCategoryId = "question",
|
||||
primaryAction = FinishedEmptyStateAction("Browse packs", AppRoute.QUESTION_PACKS),
|
||||
secondaryAction = FinishedEmptyStateAction("Daily question", AppRoute.DAILY_QUESTION),
|
||||
accent = CloserPalette.PurpleDeep,
|
||||
details = listOf(
|
||||
"Write a question in your own words",
|
||||
"Check whether the tone invites honesty",
|
||||
"Save prompts that deserve a real conversation"
|
||||
)
|
||||
"Choose prompts already shaped for real conversation.",
|
||||
"Keep the next step focused on answering, not drafting.",
|
||||
"Return here when saved custom prompts are ready."
|
||||
),
|
||||
onNavigate = onNavigate
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
package app.closer.ui.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.closer.core.navigation.AppRoute
|
||||
import app.closer.ui.components.PlaceholderAction
|
||||
import app.closer.ui.components.PlaceholderScreen
|
||||
import app.closer.ui.components.FinishedEmptyStateAction
|
||||
import app.closer.ui.components.FinishedEmptyStateScreen
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
|
||||
@Composable
|
||||
fun SubscriptionScreen(
|
||||
onNavigate: (String) -> Unit = {}
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Manage the plan",
|
||||
section = "Settings",
|
||||
description = "See plan access, restore purchases, and choose the level that fits your relationship.",
|
||||
route = AppRoute.SUBSCRIPTION,
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFB98AF4),
|
||||
primaryAction = PlaceholderAction("Paywall", AppRoute.PAYWALL),
|
||||
secondaryAction = PlaceholderAction("Settings", AppRoute.SETTINGS),
|
||||
chips = listOf("Entitlement", "Plan", "Restore"),
|
||||
FinishedEmptyStateScreen(
|
||||
eyebrow = "Subscription",
|
||||
title = "Manage access from the paywall",
|
||||
body = "Plan details, restore purchases, and upgrades live together so billing choices stay clear.",
|
||||
glyphCategoryId = "star",
|
||||
primaryAction = FinishedEmptyStateAction("Open paywall", AppRoute.PAYWALL),
|
||||
secondaryAction = FinishedEmptyStateAction("Back to settings", AppRoute.SETTINGS),
|
||||
accent = CloserPalette.PurpleDeep,
|
||||
details = listOf(
|
||||
"See what your plan includes in plain language",
|
||||
"Keep restore purchase close to plan details",
|
||||
"Open the upgrade path when you are ready"
|
||||
)
|
||||
"Review the upgrade path before making a choice.",
|
||||
"Restore purchase access from the same flow.",
|
||||
"Keep account settings separate from billing decisions."
|
||||
),
|
||||
onNavigate = onNavigate
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue