From 15c1fbdda052ea7618a26493cc3c06cdf91849cf Mon Sep 17 00:00:00 2001 From: null Date: Thu, 18 Jun 2026 03:20:13 -0500 Subject: [PATCH] feat: replace PlaceholderScreen with FinishedEmptyStateScreen across PartnerHome, EmailInvite, QuestionComposer, Subscription --- .../ui/components/FinishedEmptyStateScreen.kt | 188 ++++++++++++++++++ .../ui/games/WaitingForPartnerScreen.kt | 39 ++-- .../app/closer/ui/home/PartnerHomeScreen.kt | 33 ++- .../closer/ui/pairing/EmailInviteScreen.kt | 33 ++- .../ui/questions/QuestionComposerScreen.kt | 33 ++- .../closer/ui/settings/SubscriptionScreen.kt | 33 ++- 6 files changed, 266 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/app/closer/ui/components/FinishedEmptyStateScreen.kt diff --git a/app/src/main/java/app/closer/ui/components/FinishedEmptyStateScreen.kt b/app/src/main/java/app/closer/ui/components/FinishedEmptyStateScreen.kt new file mode 100644 index 00000000..f91f6330 --- /dev/null +++ b/app/src/main/java/app/closer/ui/components/FinishedEmptyStateScreen.kt @@ -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 = 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)) + } +} diff --git a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt index 7a4222d7..ed7df670 100644 --- a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt +++ b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt @@ -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" +} diff --git a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt index 200b6086..1ce1374d 100644 --- a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/pairing/EmailInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/EmailInviteScreen.kt index e3007a20..f0a9716e 100644 --- a/app/src/main/java/app/closer/ui/pairing/EmailInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/EmailInviteScreen.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/questions/QuestionComposerScreen.kt b/app/src/main/java/app/closer/ui/questions/QuestionComposerScreen.kt index ac02e820..fa77344c 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionComposerScreen.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionComposerScreen.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/settings/SubscriptionScreen.kt b/app/src/main/java/app/closer/ui/settings/SubscriptionScreen.kt index 1b214911..1ab0f826 100644 --- a/app/src/main/java/app/closer/ui/settings/SubscriptionScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SubscriptionScreen.kt @@ -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 ) }