feat: home screen + viewmodel, iOS home & pairing views
This commit is contained in:
parent
7d5fc11366
commit
d307904ff8
|
|
@ -17,6 +17,7 @@ import app.closer.ui.questions.displayCategoryName
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -35,7 +36,10 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.TrendingUp
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.LocalFireDepartment
|
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.PersonAdd
|
||||||
import app.closer.domain.model.OutcomeDay
|
import app.closer.domain.model.OutcomeDay
|
||||||
import app.closer.ui.components.OutcomeCheckInDialog
|
import app.closer.ui.components.OutcomeCheckInDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -167,6 +171,7 @@ fun HomeScreen(
|
||||||
onHistory = { onNavigate(AppRoute.ANSWER_HISTORY) },
|
onHistory = { onNavigate(AppRoute.ANSWER_HISTORY) },
|
||||||
onSettings = { onNavigate(AppRoute.SETTINGS) },
|
onSettings = { onNavigate(AppRoute.SETTINGS) },
|
||||||
onInvite = { onNavigate(AppRoute.CREATE_INVITE) },
|
onInvite = { onNavigate(AppRoute.CREATE_INVITE) },
|
||||||
|
onAcceptInvite = { onNavigate(AppRoute.ACCEPT_INVITE) },
|
||||||
onReminder = viewModel::sendGentleReminder,
|
onReminder = viewModel::sendGentleReminder,
|
||||||
onReveal = { state.dailyQuestion?.id?.let { onNavigate(AppRoute.answerReveal(it)) } },
|
onReveal = { state.dailyQuestion?.id?.let { onNavigate(AppRoute.answerReveal(it)) } },
|
||||||
onFollowUp = { state.dailyQuestion?.let { onNavigate(AppRoute.questionThread(state.coupleId ?: "", it.id)) } },
|
onFollowUp = { state.dailyQuestion?.let { onNavigate(AppRoute.questionThread(state.coupleId ?: "", it.id)) } },
|
||||||
|
|
@ -185,6 +190,7 @@ data class HomeCallbacks(
|
||||||
val onHistory: () -> Unit,
|
val onHistory: () -> Unit,
|
||||||
val onSettings: () -> Unit,
|
val onSettings: () -> Unit,
|
||||||
val onInvite: () -> Unit,
|
val onInvite: () -> Unit,
|
||||||
|
val onAcceptInvite: () -> Unit,
|
||||||
val onRefresh: () -> Unit
|
val onRefresh: () -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -214,6 +220,7 @@ private fun HomeContent(
|
||||||
onHistory: () -> Unit,
|
onHistory: () -> Unit,
|
||||||
onSettings: () -> Unit,
|
onSettings: () -> Unit,
|
||||||
onInvite: () -> Unit,
|
onInvite: () -> Unit,
|
||||||
|
onAcceptInvite: () -> Unit,
|
||||||
onReminder: () -> Unit,
|
onReminder: () -> Unit,
|
||||||
onReveal: () -> Unit,
|
onReveal: () -> Unit,
|
||||||
onFollowUp: () -> Unit,
|
onFollowUp: () -> Unit,
|
||||||
|
|
@ -222,7 +229,7 @@ private fun HomeContent(
|
||||||
) {
|
) {
|
||||||
val callbacks = remember(
|
val callbacks = remember(
|
||||||
onDailyQuestion, onReminder, onReveal, onFollowUp,
|
onDailyQuestion, onReminder, onReveal, onFollowUp,
|
||||||
onPacks, onCategory, onHistory, onSettings, onInvite, onRefresh
|
onPacks, onCategory, onHistory, onSettings, onInvite, onAcceptInvite, onRefresh
|
||||||
) {
|
) {
|
||||||
HomeCallbacks(
|
HomeCallbacks(
|
||||||
onDailyQuestion = onDailyQuestion,
|
onDailyQuestion = onDailyQuestion,
|
||||||
|
|
@ -234,6 +241,7 @@ private fun HomeContent(
|
||||||
onHistory = onHistory,
|
onHistory = onHistory,
|
||||||
onSettings = onSettings,
|
onSettings = onSettings,
|
||||||
onInvite = onInvite,
|
onInvite = onInvite,
|
||||||
|
onAcceptInvite = onAcceptInvite,
|
||||||
onRefresh = onRefresh
|
onRefresh = onRefresh
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -274,14 +282,17 @@ private fun HomeContent(
|
||||||
) {
|
) {
|
||||||
HomeHeader(
|
HomeHeader(
|
||||||
partnerName = state.partnerName,
|
partnerName = state.partnerName,
|
||||||
streakCount = state.streakCount
|
streakCount = state.streakCount,
|
||||||
|
isPaired = state.isPaired
|
||||||
)
|
)
|
||||||
|
|
||||||
StreakCard(
|
if (state.isPaired) {
|
||||||
streakCount = state.streakCount,
|
StreakCard(
|
||||||
partnerName = state.partnerName,
|
streakCount = state.streakCount,
|
||||||
onPartner = onPartner
|
partnerName = state.partnerName,
|
||||||
)
|
onPartner = onPartner
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingHomeCard()
|
state.isLoading -> LoadingHomeCard()
|
||||||
|
|
@ -295,17 +306,24 @@ private fun HomeContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
state.primaryAction?.let { action ->
|
state.primaryAction?.let { action ->
|
||||||
PrimaryHomeActionCard(
|
if (!state.isPaired && action.target == HomeActionTarget.InvitePartner) {
|
||||||
action = action,
|
PartnerActivationCard(
|
||||||
stats = state.answerStats,
|
onInvite = callbacks.onInvite,
|
||||||
streakCount = state.streakCount,
|
onAcceptInvite = callbacks.onAcceptInvite
|
||||||
onAction = onActionSelected,
|
)
|
||||||
onReminder = callbacks.onReminder,
|
} else {
|
||||||
onReveal = callbacks.onReveal,
|
PrimaryHomeActionCard(
|
||||||
onFollowUp = callbacks.onFollowUp,
|
action = action,
|
||||||
dailyQuestionState = state.dailyQuestionState,
|
stats = state.answerStats,
|
||||||
dailyQuestion = state.dailyQuestion
|
streakCount = state.streakCount,
|
||||||
)
|
onAction = onActionSelected,
|
||||||
|
onReminder = callbacks.onReminder,
|
||||||
|
onReveal = callbacks.onReveal,
|
||||||
|
onFollowUp = callbacks.onFollowUp,
|
||||||
|
dailyQuestionState = state.dailyQuestionState,
|
||||||
|
dailyQuestion = state.dailyQuestion
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionFeedSection(
|
ActionFeedSection(
|
||||||
|
|
@ -412,7 +430,8 @@ private fun StreakCard(
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeHeader(
|
private fun HomeHeader(
|
||||||
partnerName: String?,
|
partnerName: String?,
|
||||||
streakCount: Int
|
streakCount: Int,
|
||||||
|
isPaired: Boolean
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -431,7 +450,9 @@ private fun HomeHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (partnerName != null)
|
text = if (!isPaired)
|
||||||
|
"Set up your shared space, then keep exploring at your own pace."
|
||||||
|
else if (partnerName != null)
|
||||||
"Connected with $partnerName. One clear next step, then the rest can stay quiet."
|
"Connected with $partnerName. One clear next step, then the rest can stay quiet."
|
||||||
else
|
else
|
||||||
"Open the app, see what matters, and take one small step toward closeness.",
|
"Open the app, see what matters, and take one small step toward closeness.",
|
||||||
|
|
@ -443,6 +464,182 @@ private fun HomeHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PartnerActivationCard(
|
||||||
|
onInvite: () -> Unit,
|
||||||
|
onAcceptInvite: () -> Unit
|
||||||
|
) {
|
||||||
|
CloserCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(CloserRadii.FeatureCard),
|
||||||
|
containerColor = closerCardColor(alpha = 0.94f),
|
||||||
|
elevation = CloserElevations.Feature
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
CloserPalette.PurpleSoft,
|
||||||
|
CloserPalette.PinkMist.copy(alpha = 0.84f)
|
||||||
|
),
|
||||||
|
start = Offset.Zero,
|
||||||
|
end = Offset.Infinite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HomePill("1 of 2 connected")
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(CloserRadii.Pill),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.76f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Lock,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = CloserPalette.PurpleDeep,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Private invite",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = CloserPalette.PurpleDeep,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
PartnerActivationAvatars()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "A private space for two",
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color(0xFF4D4354),
|
||||||
|
maxLines = 4,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
ActivationBenefitPill("Private reveals", Modifier.weight(1f))
|
||||||
|
ActivationBenefitPill("Shared streak", Modifier.weight(1f))
|
||||||
|
ActivationBenefitPill("Games for two", Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
CloserActionButton(
|
||||||
|
label = "Invite partner",
|
||||||
|
onClick = onInvite,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
containerColor = CloserPalette.PurpleDeep,
|
||||||
|
contentColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
CloserActionButton(
|
||||||
|
label = "Enter code",
|
||||||
|
onClick = onAcceptInvite,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
style = CloserButtonStyle.Secondary,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
|
||||||
|
contentColor = CloserPalette.PurpleDeep
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PartnerActivationAvatars() {
|
||||||
|
Box(modifier = Modifier.size(width = 92.dp, height = 58.dp)) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.align(Alignment.CenterStart),
|
||||||
|
shape = RoundedCornerShape(999.dp),
|
||||||
|
color = CloserPalette.PurpleDeep
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.size(27.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.align(Alignment.CenterEnd),
|
||||||
|
shape = RoundedCornerShape(999.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
|
||||||
|
border = BorderStroke(1.5.dp, CloserPalette.PurpleDeep.copy(alpha = 0.42f))
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PersonAdd,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = CloserPalette.PurpleDeep,
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActivationBenefitPill(
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(CloserRadii.Pill),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.66f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.padding(horizontal = 9.dp, vertical = 7.dp),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = CloserPalette.PurpleDeep,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PrimaryHomeActionCard(
|
private fun PrimaryHomeActionCard(
|
||||||
action: HomeAction,
|
action: HomeAction,
|
||||||
|
|
@ -1125,6 +1322,7 @@ fun HomeScreenPreview() {
|
||||||
onHistory = {},
|
onHistory = {},
|
||||||
onSettings = {},
|
onSettings = {},
|
||||||
onInvite = {},
|
onInvite = {},
|
||||||
|
onAcceptInvite = {},
|
||||||
onRefresh = {}
|
onRefresh = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -549,9 +549,9 @@ class HomeViewModel @Inject constructor(
|
||||||
) else null
|
) else null
|
||||||
|
|
||||||
Priority.PAIRING_NEEDED -> HomeAction(
|
Priority.PAIRING_NEEDED -> HomeAction(
|
||||||
eyebrow = "Next best action",
|
eyebrow = "1 of 2 connected",
|
||||||
title = "Invite your partner into tonight.",
|
title = "A private space for two",
|
||||||
body = "The app works best as a shared ritual. Send a private invite and make the next prompt something you can both answer.",
|
body = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||||
cta = "Invite partner",
|
cta = "Invite partner",
|
||||||
target = HomeActionTarget.InvitePartner,
|
target = HomeActionTarget.InvitePartner,
|
||||||
tone = HomeActionTone.Invite
|
tone = HomeActionTone.Invite
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ struct HomeView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@State private var showPartnerHome = false
|
@State private var showPartnerHome = false
|
||||||
@State private var showBucketList = false
|
@State private var showBucketList = false
|
||||||
|
@State private var showCreateInvite = false
|
||||||
|
@State private var showAcceptInvite = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
|
@ -22,9 +24,17 @@ struct HomeView: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.closerPadding()
|
.closerPadding()
|
||||||
|
|
||||||
// Streak card
|
if appState.currentCouple == nil {
|
||||||
StreakCard()
|
PartnerActivationCard(
|
||||||
|
onInvite: { showCreateInvite = true },
|
||||||
|
onAcceptInvite: { showAcceptInvite = true }
|
||||||
|
)
|
||||||
.closerPadding()
|
.closerPadding()
|
||||||
|
} else {
|
||||||
|
// Streak card
|
||||||
|
StreakCard()
|
||||||
|
.closerPadding()
|
||||||
|
}
|
||||||
|
|
||||||
// Quick actions
|
// Quick actions
|
||||||
VStack(alignment: .leading, spacing: CloserSpacing.md) {
|
VStack(alignment: .leading, spacing: CloserSpacing.md) {
|
||||||
|
|
@ -59,41 +69,43 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partner status
|
if appState.currentCouple != nil {
|
||||||
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
// Partner status
|
||||||
Text("Partner")
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||||
.closerSectionTitle()
|
Text("Partner")
|
||||||
|
.closerSectionTitle()
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
PartnerStatusRow(
|
||||||
|
displayName: appState.currentPartner?.displayName ?? "Your Partner",
|
||||||
|
answered: false,
|
||||||
|
lastActive: nil
|
||||||
|
)
|
||||||
.closerPadding()
|
.closerPadding()
|
||||||
|
|
||||||
PartnerStatusRow(
|
|
||||||
displayName: "Your Partner",
|
|
||||||
answered: false,
|
|
||||||
lastActive: nil
|
|
||||||
)
|
|
||||||
.closerPadding()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relationship summary
|
|
||||||
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
|
||||||
Text("Your Journey")
|
|
||||||
.closerSectionTitle()
|
|
||||||
.closerPadding()
|
|
||||||
|
|
||||||
VStack(spacing: CloserSpacing.sm) {
|
|
||||||
StatRow(icon: "flame.fill", label: "Streak", value: "\(appState.currentCouple?.streakCount ?? 0) days")
|
|
||||||
StatRow(icon: "questionmark.bubble.fill", label: "Questions Answered", value: "12")
|
|
||||||
StatRow(icon: "gamecontroller.fill", label: "Games Played", value: "5")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relationship summary
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||||
|
Text("Your Journey")
|
||||||
|
.closerSectionTitle()
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
VStack(spacing: CloserSpacing.sm) {
|
||||||
|
StatRow(icon: "flame.fill", label: "Streak", value: "\(appState.currentCouple?.streakCount ?? 0) days")
|
||||||
|
StatRow(icon: "questionmark.bubble.fill", label: "Questions Answered", value: "12")
|
||||||
|
StatRow(icon: "gamecontroller.fill", label: "Games Played", value: "5")
|
||||||
|
}
|
||||||
|
.closerPadding()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom nav to partner home
|
||||||
|
Button(action: { showPartnerHome = true }) {
|
||||||
|
Label("View Partner's Activity", systemImage: "person.fill")
|
||||||
|
}
|
||||||
|
.buttonStyle(SecondaryButtonStyle())
|
||||||
.closerPadding()
|
.closerPadding()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom nav to partner home
|
|
||||||
Button(action: { showPartnerHome = true }) {
|
|
||||||
Label("View Partner's Activity", systemImage: "person.fill")
|
|
||||||
}
|
|
||||||
.buttonStyle(SecondaryButtonStyle())
|
|
||||||
.closerPadding()
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: CloserSpacing.xxl)
|
.frame(height: CloserSpacing.xxl)
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +118,12 @@ struct HomeView: View {
|
||||||
.navigationDestination(isPresented: $showBucketList) {
|
.navigationDestination(isPresented: $showBucketList) {
|
||||||
BucketListView()
|
BucketListView()
|
||||||
}
|
}
|
||||||
|
.navigationDestination(isPresented: $showCreateInvite) {
|
||||||
|
CreateInviteView()
|
||||||
|
}
|
||||||
|
.navigationDestination(isPresented: $showAcceptInvite) {
|
||||||
|
AcceptInviteView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,4 +241,4 @@ struct StatRow: View {
|
||||||
.background(Color.closerSurface)
|
.background(Color.closerSurface)
|
||||||
.cornerRadius(CloserRadius.medium)
|
.cornerRadius(CloserRadius.medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,36 +8,35 @@ struct PairPromptView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VStack(spacing: CloserSpacing.xxl) {
|
ScrollView {
|
||||||
Spacer()
|
VStack(spacing: CloserSpacing.xl) {
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||||
|
Text("You're in")
|
||||||
|
.font(CloserFont.callout)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
Text("Set up your shared space")
|
||||||
|
.font(CloserFont.title1)
|
||||||
|
.foregroundColor(.closerText)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.closerPadding()
|
||||||
|
.padding(.top, CloserSpacing.xl)
|
||||||
|
|
||||||
CloserIllustrationView(imageName: "illustration-couple-invite", size: 190)
|
PartnerActivationCard(
|
||||||
|
onInvite: { showCreateInvite = true },
|
||||||
Text("Connect with Your Partner")
|
onAcceptInvite: { showAcceptInvite = true }
|
||||||
.font(CloserFont.title1)
|
)
|
||||||
.foregroundColor(.closerText)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
Text("Create an invite code for your partner to join, or enter their code to connect.")
|
|
||||||
.font(CloserFont.callout)
|
|
||||||
.foregroundColor(.closerTextSecondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.closerPadding()
|
.closerPadding()
|
||||||
|
|
||||||
VStack(spacing: CloserSpacing.lg) {
|
Text("You can keep exploring Closer, but shared reveals and partner activity start after your person joins.")
|
||||||
Button(action: { showCreateInvite = true }) {
|
.font(CloserFont.footnote)
|
||||||
Label("Invite my partner", systemImage: "plus.circle.fill")
|
.foregroundColor(.closerTextSecondary)
|
||||||
}
|
.multilineTextAlignment(.center)
|
||||||
.buttonStyle(PrimaryButtonStyle())
|
.closerPadding()
|
||||||
|
|
||||||
Button(action: { showAcceptInvite = true }) {
|
Spacer()
|
||||||
Label("Enter a code", systemImage: "key.fill")
|
.frame(height: CloserSpacing.xxl)
|
||||||
}
|
|
||||||
.buttonStyle(SecondaryButtonStyle())
|
|
||||||
}
|
}
|
||||||
.closerPadding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.background(Color.closerBackground)
|
.background(Color.closerBackground)
|
||||||
.navigationDestination(isPresented: $showCreateInvite) {
|
.navigationDestination(isPresented: $showCreateInvite) {
|
||||||
|
|
@ -50,6 +49,140 @@ struct PairPromptView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Partner Activation
|
||||||
|
|
||||||
|
struct PartnerActivationCard: View {
|
||||||
|
let onInvite: () -> Void
|
||||||
|
let onAcceptInvite: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.lg) {
|
||||||
|
HStack {
|
||||||
|
Text("1 of 2 connected")
|
||||||
|
.font(CloserFont.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.closerPrimary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 7)
|
||||||
|
.background(Color.closerBackground.opacity(0.75))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Label("Private invite", systemImage: "lock.fill")
|
||||||
|
.font(CloserFont.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.closerPrimary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 7)
|
||||||
|
.background(Color.closerBackground.opacity(0.75))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: CloserSpacing.lg) {
|
||||||
|
ActivationAvatarPair()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||||
|
Text("A private space for two")
|
||||||
|
.font(CloserFont.title2)
|
||||||
|
.foregroundColor(.closerText)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
Text("Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.")
|
||||||
|
.font(CloserFont.callout)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: CloserSpacing.sm) {
|
||||||
|
ActivationBenefitChip("Private reveals")
|
||||||
|
ActivationBenefitChip("Shared streak")
|
||||||
|
ActivationBenefitChip("Games for two")
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: CloserSpacing.md) {
|
||||||
|
Button(action: onInvite) {
|
||||||
|
Label("Invite partner", systemImage: "person.crop.circle.badge.plus")
|
||||||
|
}
|
||||||
|
.buttonStyle(PrimaryButtonStyle())
|
||||||
|
|
||||||
|
Button(action: onAcceptInvite) {
|
||||||
|
Label("Enter code", systemImage: "key.fill")
|
||||||
|
}
|
||||||
|
.buttonStyle(SecondaryButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(CloserSpacing.lg)
|
||||||
|
.background(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color.closerSurface,
|
||||||
|
Color.closerPrimary.opacity(0.18),
|
||||||
|
Color.closerSecondary.opacity(0.18)
|
||||||
|
],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
||||||
|
.closerShadow(level: .medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ActivationAvatarPair: View {
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.closerPrimary)
|
||||||
|
.frame(width: 54, height: 54)
|
||||||
|
.overlay {
|
||||||
|
Image(systemName: "person.fill")
|
||||||
|
.font(.system(size: 25, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.offset(x: -18)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(Color.closerBackground.opacity(0.9))
|
||||||
|
.frame(width: 54, height: 54)
|
||||||
|
.overlay {
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.closerPrimary.opacity(0.42), lineWidth: 1.5)
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
Image(systemName: "person.crop.circle.badge.plus")
|
||||||
|
.font(.system(size: 23, weight: .semibold))
|
||||||
|
.foregroundColor(.closerPrimary)
|
||||||
|
}
|
||||||
|
.offset(x: 18)
|
||||||
|
}
|
||||||
|
.frame(width: 92, height: 62)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ActivationBenefitChip: View {
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
init(_ label: String) {
|
||||||
|
self.label = label
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(label)
|
||||||
|
.font(CloserFont.caption2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.closerPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.78)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color.closerBackground.opacity(0.68))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Create Invite
|
// MARK: - Create Invite
|
||||||
|
|
||||||
struct CreateInviteView: View {
|
struct CreateInviteView: View {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue