feat: replace PlaceholderScreen with FinishedEmptyStateScreen across PartnerHome, EmailInvite, QuestionComposer, Subscription

This commit is contained in:
null 2026-06-18 03:20:13 -05:00
parent 8b394ab40f
commit 15c1fbdda0
6 changed files with 266 additions and 93 deletions

View File

@ -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))
}
}

View File

@ -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"
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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
)
}