feat: home screen + viewmodel, iOS home & pairing views
This commit is contained in:
parent
5ac4f40bf6
commit
af35ec029b
|
|
@ -17,6 +17,7 @@ import app.closer.ui.questions.displayCategoryName
|
|||
import app.closer.ui.theme.CloserPalette
|
||||
import app.closer.ui.theme.closerBackgroundBrush
|
||||
import app.closer.ui.theme.closerCardColor
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.automirrored.filled.ArrowForward
|
||||
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.Person
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import app.closer.domain.model.OutcomeDay
|
||||
import app.closer.ui.components.OutcomeCheckInDialog
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -167,6 +171,7 @@ fun HomeScreen(
|
|||
onHistory = { onNavigate(AppRoute.ANSWER_HISTORY) },
|
||||
onSettings = { onNavigate(AppRoute.SETTINGS) },
|
||||
onInvite = { onNavigate(AppRoute.CREATE_INVITE) },
|
||||
onAcceptInvite = { onNavigate(AppRoute.ACCEPT_INVITE) },
|
||||
onReminder = viewModel::sendGentleReminder,
|
||||
onReveal = { state.dailyQuestion?.id?.let { onNavigate(AppRoute.answerReveal(it)) } },
|
||||
onFollowUp = { state.dailyQuestion?.let { onNavigate(AppRoute.questionThread(state.coupleId ?: "", it.id)) } },
|
||||
|
|
@ -185,6 +190,7 @@ data class HomeCallbacks(
|
|||
val onHistory: () -> Unit,
|
||||
val onSettings: () -> Unit,
|
||||
val onInvite: () -> Unit,
|
||||
val onAcceptInvite: () -> Unit,
|
||||
val onRefresh: () -> Unit
|
||||
)
|
||||
|
||||
|
|
@ -214,6 +220,7 @@ private fun HomeContent(
|
|||
onHistory: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
onInvite: () -> Unit,
|
||||
onAcceptInvite: () -> Unit,
|
||||
onReminder: () -> Unit,
|
||||
onReveal: () -> Unit,
|
||||
onFollowUp: () -> Unit,
|
||||
|
|
@ -222,7 +229,7 @@ private fun HomeContent(
|
|||
) {
|
||||
val callbacks = remember(
|
||||
onDailyQuestion, onReminder, onReveal, onFollowUp,
|
||||
onPacks, onCategory, onHistory, onSettings, onInvite, onRefresh
|
||||
onPacks, onCategory, onHistory, onSettings, onInvite, onAcceptInvite, onRefresh
|
||||
) {
|
||||
HomeCallbacks(
|
||||
onDailyQuestion = onDailyQuestion,
|
||||
|
|
@ -234,6 +241,7 @@ private fun HomeContent(
|
|||
onHistory = onHistory,
|
||||
onSettings = onSettings,
|
||||
onInvite = onInvite,
|
||||
onAcceptInvite = onAcceptInvite,
|
||||
onRefresh = onRefresh
|
||||
)
|
||||
}
|
||||
|
|
@ -274,14 +282,17 @@ private fun HomeContent(
|
|||
) {
|
||||
HomeHeader(
|
||||
partnerName = state.partnerName,
|
||||
streakCount = state.streakCount
|
||||
streakCount = state.streakCount,
|
||||
isPaired = state.isPaired
|
||||
)
|
||||
|
||||
StreakCard(
|
||||
streakCount = state.streakCount,
|
||||
partnerName = state.partnerName,
|
||||
onPartner = onPartner
|
||||
)
|
||||
if (state.isPaired) {
|
||||
StreakCard(
|
||||
streakCount = state.streakCount,
|
||||
partnerName = state.partnerName,
|
||||
onPartner = onPartner
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
state.isLoading -> LoadingHomeCard()
|
||||
|
|
@ -295,17 +306,24 @@ private fun HomeContent(
|
|||
}
|
||||
|
||||
state.primaryAction?.let { action ->
|
||||
PrimaryHomeActionCard(
|
||||
action = action,
|
||||
stats = state.answerStats,
|
||||
streakCount = state.streakCount,
|
||||
onAction = onActionSelected,
|
||||
onReminder = callbacks.onReminder,
|
||||
onReveal = callbacks.onReveal,
|
||||
onFollowUp = callbacks.onFollowUp,
|
||||
dailyQuestionState = state.dailyQuestionState,
|
||||
dailyQuestion = state.dailyQuestion
|
||||
)
|
||||
if (!state.isPaired && action.target == HomeActionTarget.InvitePartner) {
|
||||
PartnerActivationCard(
|
||||
onInvite = callbacks.onInvite,
|
||||
onAcceptInvite = callbacks.onAcceptInvite
|
||||
)
|
||||
} else {
|
||||
PrimaryHomeActionCard(
|
||||
action = action,
|
||||
stats = state.answerStats,
|
||||
streakCount = state.streakCount,
|
||||
onAction = onActionSelected,
|
||||
onReminder = callbacks.onReminder,
|
||||
onReveal = callbacks.onReveal,
|
||||
onFollowUp = callbacks.onFollowUp,
|
||||
dailyQuestionState = state.dailyQuestionState,
|
||||
dailyQuestion = state.dailyQuestion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ActionFeedSection(
|
||||
|
|
@ -412,7 +430,8 @@ private fun StreakCard(
|
|||
@Composable
|
||||
private fun HomeHeader(
|
||||
partnerName: String?,
|
||||
streakCount: Int
|
||||
streakCount: Int,
|
||||
isPaired: Boolean
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
|
|
@ -431,7 +450,9 @@ private fun HomeHeader(
|
|||
}
|
||||
}
|
||||
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."
|
||||
else
|
||||
"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
|
||||
private fun PrimaryHomeActionCard(
|
||||
action: HomeAction,
|
||||
|
|
@ -1125,6 +1322,7 @@ fun HomeScreenPreview() {
|
|||
onHistory = {},
|
||||
onSettings = {},
|
||||
onInvite = {},
|
||||
onAcceptInvite = {},
|
||||
onRefresh = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -549,9 +549,9 @@ class HomeViewModel @Inject constructor(
|
|||
) else null
|
||||
|
||||
Priority.PAIRING_NEEDED -> HomeAction(
|
||||
eyebrow = "Next best action",
|
||||
title = "Invite your partner into tonight.",
|
||||
body = "The app works best as a shared ritual. Send a private invite and make the next prompt something you can both answer.",
|
||||
eyebrow = "1 of 2 connected",
|
||||
title = "A private space for two",
|
||||
body = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||
cta = "Invite partner",
|
||||
target = HomeActionTarget.InvitePartner,
|
||||
tone = HomeActionTone.Invite
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ struct HomeView: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@State private var showPartnerHome = false
|
||||
@State private var showBucketList = false
|
||||
@State private var showCreateInvite = false
|
||||
@State private var showAcceptInvite = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
|
@ -22,9 +24,17 @@ struct HomeView: View {
|
|||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.closerPadding()
|
||||
|
||||
// Streak card
|
||||
StreakCard()
|
||||
if appState.currentCouple == nil {
|
||||
PartnerActivationCard(
|
||||
onInvite: { showCreateInvite = true },
|
||||
onAcceptInvite: { showAcceptInvite = true }
|
||||
)
|
||||
.closerPadding()
|
||||
} else {
|
||||
// Streak card
|
||||
StreakCard()
|
||||
.closerPadding()
|
||||
}
|
||||
|
||||
// Quick actions
|
||||
VStack(alignment: .leading, spacing: CloserSpacing.md) {
|
||||
|
|
@ -59,41 +69,43 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Partner status
|
||||
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||
Text("Partner")
|
||||
.closerSectionTitle()
|
||||
if appState.currentCouple != nil {
|
||||
// Partner status
|
||||
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||
Text("Partner")
|
||||
.closerSectionTitle()
|
||||
.closerPadding()
|
||||
|
||||
PartnerStatusRow(
|
||||
displayName: appState.currentPartner?.displayName ?? "Your Partner",
|
||||
answered: false,
|
||||
lastActive: nil
|
||||
)
|
||||
.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()
|
||||
}
|
||||
|
||||
// Bottom nav to partner home
|
||||
Button(action: { showPartnerHome = true }) {
|
||||
Label("View Partner's Activity", systemImage: "person.fill")
|
||||
}
|
||||
.buttonStyle(SecondaryButtonStyle())
|
||||
.closerPadding()
|
||||
|
||||
|
||||
Spacer()
|
||||
.frame(height: CloserSpacing.xxl)
|
||||
}
|
||||
|
|
@ -106,6 +118,12 @@ struct HomeView: View {
|
|||
.navigationDestination(isPresented: $showBucketList) {
|
||||
BucketListView()
|
||||
}
|
||||
.navigationDestination(isPresented: $showCreateInvite) {
|
||||
CreateInviteView()
|
||||
}
|
||||
.navigationDestination(isPresented: $showAcceptInvite) {
|
||||
AcceptInviteView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,4 +241,4 @@ struct StatRow: View {
|
|||
.background(Color.closerSurface)
|
||||
.cornerRadius(CloserRadius.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,36 +8,35 @@ struct PairPromptView: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: CloserSpacing.xxl) {
|
||||
Spacer()
|
||||
ScrollView {
|
||||
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)
|
||||
|
||||
Text("Connect with Your Partner")
|
||||
.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)
|
||||
PartnerActivationCard(
|
||||
onInvite: { showCreateInvite = true },
|
||||
onAcceptInvite: { showAcceptInvite = true }
|
||||
)
|
||||
.closerPadding()
|
||||
|
||||
VStack(spacing: CloserSpacing.lg) {
|
||||
Button(action: { showCreateInvite = true }) {
|
||||
Label("Invite my partner", systemImage: "plus.circle.fill")
|
||||
}
|
||||
.buttonStyle(PrimaryButtonStyle())
|
||||
Text("You can keep exploring Closer, but shared reveals and partner activity start after your person joins.")
|
||||
.font(CloserFont.footnote)
|
||||
.foregroundColor(.closerTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.closerPadding()
|
||||
|
||||
Button(action: { showAcceptInvite = true }) {
|
||||
Label("Enter a code", systemImage: "key.fill")
|
||||
}
|
||||
.buttonStyle(SecondaryButtonStyle())
|
||||
Spacer()
|
||||
.frame(height: CloserSpacing.xxl)
|
||||
}
|
||||
.closerPadding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(Color.closerBackground)
|
||||
.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
|
||||
|
||||
struct CreateInviteView: View {
|
||||
|
|
|
|||
Loading…
Reference in New Issue