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.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -31,6 +29,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.domain.usecase.GameSessionManager import app.closer.domain.usecase.GameSessionManager
import app.closer.ui.components.CategoryGlyph
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -127,29 +126,11 @@ fun WaitingForPartnerScreen(
) )
} }
else -> { else -> {
// Game type icon/symbol CategoryGlyph(
Surface( categoryId = gameTypeGlyphKey(state.gameType),
shape = CircleShape, size = 80.dp,
color = MaterialTheme.colorScheme.primaryContainer, iconSize = 38.dp
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
) )
}
}
Text( Text(
text = "Waiting for ${state.partnerName}", text = "Waiting for ${state.partnerName}",
@ -185,3 +166,11 @@ private fun gameTypeLabel(gameType: String): String = when (gameType) {
"desire_sync" -> "Desire Sync" "desire_sync" -> "Desire Sync"
else -> gameType 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 package app.closer.ui.home
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.PlaceholderAction import app.closer.ui.components.FinishedEmptyStateAction
import app.closer.ui.components.PlaceholderScreen import app.closer.ui.components.FinishedEmptyStateScreen
import app.closer.ui.theme.CloserPalette
@Composable @Composable
fun PartnerHomeScreen( fun PartnerHomeScreen(
onNavigate: (String) -> Unit = {} onNavigate: (String) -> Unit = {}
) { ) {
PlaceholderScreen( FinishedEmptyStateScreen(
title = "A shared pulse", eyebrow = "Partner space",
section = "Home", title = "Connect your shared home",
description = "A partner-aware view for pairing status, recent rituals, and the next shared prompt.", body = "Invite your partner to unlock shared prompts, paired games, and a relationship rhythm built for both of you.",
route = AppRoute.PARTNER_HOME, glyphCategoryId = "people",
onNavigate = onNavigate, primaryAction = FinishedEmptyStateAction("Invite partner", AppRoute.CREATE_INVITE),
accent = Color(0xFFB98AF4), secondaryAction = FinishedEmptyStateAction("Back home", AppRoute.HOME),
primaryAction = PlaceholderAction("Invite partner", AppRoute.CREATE_INVITE), accent = CloserPalette.PurpleDeep,
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
chips = listOf("Partner state", "Pairing bridge", "Shared rhythm"),
details = listOf( details = listOf(
"Know whether you are connected at a glance", "Pair once, then share prompts without repeating setup.",
"Keep shared activity separate from private reflections", "Keep shared activity distinct from private reflections.",
"Return to the couple space without extra searching" "Return to the couple space when both of you are ready."
) ),
onNavigate = onNavigate
) )
} }

View File

@ -1,31 +1,30 @@
package app.closer.ui.pairing package app.closer.ui.pairing
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.PlaceholderAction import app.closer.ui.components.FinishedEmptyStateAction
import app.closer.ui.components.PlaceholderScreen import app.closer.ui.components.FinishedEmptyStateScreen
import app.closer.ui.theme.CloserPalette
@Composable @Composable
fun EmailInviteScreen( fun EmailInviteScreen(
onNavigate: (String) -> Unit = {} onNavigate: (String) -> Unit = {}
) { ) {
PlaceholderScreen( FinishedEmptyStateScreen(
title = "Send the thread", eyebrow = "Pairing",
section = "Pairing", title = "Send an invite that is easy to follow",
description = "Invite your partner with a clear message and a simple code.", body = "Create an invite code first, then share it with your partner in the channel you both already use.",
route = AppRoute.EMAIL_INVITE, glyphCategoryId = "communication",
onNavigate = onNavigate, primaryAction = FinishedEmptyStateAction("Create invite", AppRoute.CREATE_INVITE),
accent = Color(0xFFB98AF4), secondaryAction = FinishedEmptyStateAction("Enter a code", AppRoute.ACCEPT_INVITE),
primaryAction = PlaceholderAction("Confirm code", AppRoute.inviteConfirm("ABC123")), accent = CloserPalette.PurpleDeep,
secondaryAction = PlaceholderAction("Create invite", AppRoute.CREATE_INVITE),
chips = listOf("Email", "Code ABC123", "Invite"),
details = listOf( details = listOf(
"Keep the message short and easy to understand", "Use one code instead of a temporary sample invite.",
"Make the code clear before it is sent", "Keep the message short enough to send anywhere.",
"Give your partner one simple next step" "Give your partner a single next step."
) ),
onNavigate = onNavigate
) )
} }

View File

@ -1,31 +1,30 @@
package app.closer.ui.questions package app.closer.ui.questions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.PlaceholderAction import app.closer.ui.components.FinishedEmptyStateAction
import app.closer.ui.components.PlaceholderScreen import app.closer.ui.components.FinishedEmptyStateScreen
import app.closer.ui.theme.CloserPalette
@Composable @Composable
fun QuestionComposerScreen( fun QuestionComposerScreen(
onNavigate: (String) -> Unit = {} onNavigate: (String) -> Unit = {}
) { ) {
PlaceholderScreen( FinishedEmptyStateScreen(
title = "Ask it cleanly", eyebrow = "Questions",
section = "Questions", title = "Create a question with care",
description = "Shape your own prompt with a tone that feels generous and clear.", body = "Custom prompts are coming soon. For now, start from a pack so the tone stays clear, generous, and easy to answer.",
route = AppRoute.QUESTION_COMPOSER, glyphCategoryId = "question",
onNavigate = onNavigate, primaryAction = FinishedEmptyStateAction("Browse packs", AppRoute.QUESTION_PACKS),
accent = Color(0xFFB98AF4), secondaryAction = FinishedEmptyStateAction("Daily question", AppRoute.DAILY_QUESTION),
primaryAction = PlaceholderAction("Open thread", AppRoute.questionThread("couple", "custom")), accent = CloserPalette.PurpleDeep,
secondaryAction = PlaceholderAction("Packs", AppRoute.QUESTION_PACKS),
chips = listOf("Custom prompt", "Tone aware", "Save"),
details = listOf( details = listOf(
"Write a question in your own words", "Choose prompts already shaped for real conversation.",
"Check whether the tone invites honesty", "Keep the next step focused on answering, not drafting.",
"Save prompts that deserve a real conversation" "Return here when saved custom prompts are ready."
) ),
onNavigate = onNavigate
) )
} }

View File

@ -1,31 +1,30 @@
package app.closer.ui.settings package app.closer.ui.settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.ui.components.PlaceholderAction import app.closer.ui.components.FinishedEmptyStateAction
import app.closer.ui.components.PlaceholderScreen import app.closer.ui.components.FinishedEmptyStateScreen
import app.closer.ui.theme.CloserPalette
@Composable @Composable
fun SubscriptionScreen( fun SubscriptionScreen(
onNavigate: (String) -> Unit = {} onNavigate: (String) -> Unit = {}
) { ) {
PlaceholderScreen( FinishedEmptyStateScreen(
title = "Manage the plan", eyebrow = "Subscription",
section = "Settings", title = "Manage access from the paywall",
description = "See plan access, restore purchases, and choose the level that fits your relationship.", body = "Plan details, restore purchases, and upgrades live together so billing choices stay clear.",
route = AppRoute.SUBSCRIPTION, glyphCategoryId = "star",
onNavigate = onNavigate, primaryAction = FinishedEmptyStateAction("Open paywall", AppRoute.PAYWALL),
accent = Color(0xFFB98AF4), secondaryAction = FinishedEmptyStateAction("Back to settings", AppRoute.SETTINGS),
primaryAction = PlaceholderAction("Paywall", AppRoute.PAYWALL), accent = CloserPalette.PurpleDeep,
secondaryAction = PlaceholderAction("Settings", AppRoute.SETTINGS),
chips = listOf("Entitlement", "Plan", "Restore"),
details = listOf( details = listOf(
"See what your plan includes in plain language", "Review the upgrade path before making a choice.",
"Keep restore purchase close to plan details", "Restore purchase access from the same flow.",
"Open the upgrade path when you are ready" "Keep account settings separate from billing decisions."
) ),
onNavigate = onNavigate
) )
} }