Closer/iphone/Closer/Play/PlayViews.swift

727 lines
27 KiB
Swift

import SwiftUI
// MARK: - Play Hub
struct PlayHubView: View {
@EnvironmentObject var appState: AppState
@State private var isPremium = false
@State private var showPaywall = false
let games: [(icon: String, title: String, description: String, color: Color, isPremium: Bool, gameType: GameType)] = [
("dice.fill", "Spin the Wheel", "Let fate decide your next adventure", .closerPrimary, false, .wheel),
("hand.raised.fill", "This or That", "Discover each other's preferences", .closerSecondary, false, .thisOrThat),
("person.fill.questionmark", "How Well Do You Know Me", "Test your knowledge of each other", .categoryCommunication, false, .howWell),
("sparkles", "Desire Sync", "Align your desires and dreams", .closerSecondary, true, .desireSync),
("mountain.2.fill", "Connection Challenges", "Multi-day challenges for couples", .closerGold, true, .connectionChallenges),
("clock.fill", "Memory Lane", "Revisit your time capsules", .closerPrimary, true, .memoryLane),
]
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: CloserSpacing.xl) {
Text("Play Together")
.font(CloserFont.title1)
.foregroundColor(.closerText)
.closerPadding()
LazyVGrid(columns: [GridItem(.flexible())], spacing: CloserSpacing.md) {
ForEach(games, id: \.title) { game in
GameCard(
icon: game.icon,
title: game.title,
description: game.description,
color: game.color,
isPremium: game.isPremium,
isUnlocked: !game.isPremium || isPremium,
gameType: game.gameType
)
}
}
.closerPadding()
// Game History
NavigationLink {
GameHistoryView()
} label: {
HStack {
Image(systemName: "clock.arrow.circlepath")
Text("Past Games")
Spacer()
Image(systemName: "chevron.right")
}
.font(CloserFont.body)
.foregroundColor(.closerText)
.padding()
.closerCard()
}
.buttonStyle(.plain)
.closerPadding()
}
.padding(.vertical)
}
.background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showPaywall) {
PaywallView()
}
.task {
isPremium = await DefaultEntitlementChecker().hasPremium()
}
}
}
// MARK: - Game Card
struct GameCard: View {
let icon: String
let title: String
let description: String
let color: Color
let isPremium: Bool
let isUnlocked: Bool
let gameType: GameType
@State private var showGame = false
var body: some View {
Button(action: handleTap) {
HStack(spacing: CloserSpacing.lg) {
// Icon
ZStack {
RoundedRectangle(cornerRadius: CloserRadius.medium)
.fill(color.opacity(0.15))
.frame(width: 60, height: 60)
if gameType == .wheel {
Image("illustration-spin-wheel")
.resizable()
.scaledToFit()
.frame(width: 52, height: 52)
} else {
Image(systemName: icon)
.font(.title2)
.foregroundColor(color)
}
}
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Text(title)
.font(CloserFont.headline)
.foregroundColor(.closerText)
if isPremium {
PremiumBadge()
}
}
Text(description)
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
.lineLimit(2)
}
Spacer()
if !isUnlocked {
Image(systemName: "lock.fill")
.foregroundColor(.closerTextSecondary)
}
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.closerDivider)
}
.padding(CloserSpacing.md)
.closerCard()
}
.buttonStyle(.plain)
.navigationDestination(isPresented: $showGame) {
destinationView(for: gameType)
}
}
private func handleTap() {
guard isUnlocked else { return }
showGame = true
}
@ViewBuilder
private func destinationView(for type: GameType) -> some View {
switch type {
case .wheel:
CategoryPickerView()
case .thisOrThat:
ThisOrThatView()
case .howWell:
HowWellView()
case .desireSync:
DesireSyncView()
case .connectionChallenges:
ConnectionChallengesView()
}
}
}
// MARK: - Game History
struct GameHistoryView: View {
@State private var sessions: [QuestionSession] = []
@State private var isLoading = true
var body: some View {
List {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
} else if sessions.isEmpty {
EmptyStateView(
icon: "clock.arrow.circlepath",
title: "No Games Yet",
message: "Your game history will appear here once you start playing."
)
.listRowBackground(Color.clear)
} else {
ForEach(sessions) { session in
NavigationLink {
destinationForReplay(session.gameType, sessionId: session.id)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(session.gameType.replacing("_", with: " ").capitalized)
.font(CloserFont.body)
.foregroundColor(.closerText)
Text(session.startedAt, style: .date)
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
}
.padding(.vertical, 4)
}
}
}
}
.listStyle(.insetGrouped)
.background(Color.closerBackground)
.navigationTitle("Game History")
.navigationBarTitleDisplayMode(.inline)
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
@ViewBuilder
private func destinationForReplay(_ gameType: String, sessionId: String) -> some View {
switch gameType {
case "this_or_that": ThisOrThatReplayView(sessionId: sessionId)
case "how_well": HowWellReplayView(sessionId: sessionId)
case "desire_sync": DesireSyncReplayView(sessionId: sessionId)
default: Text("Replay not available")
}
}
}
// MARK: - This or That
struct ThisOrThatView: View {
@State private var currentPair = 0
@State private var choices: [String] = []
@State private var showResults = false
let pairs = [
("Beach vacation", "Mountain retreat"),
("Dinner out", "Cooking together"),
("Movie night", "Board game night"),
("Early bird", "Night owl"),
("Cats", "Dogs"),
("Coffee", "Tea"),
("Summer", "Winter"),
("City life", "Country life"),
]
var body: some View {
VStack(spacing: CloserSpacing.xxl) {
if showResults {
// Results view
VStack(spacing: CloserSpacing.lg) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 64))
.foregroundColor(.closerSuccess)
Text("All Done!")
.font(CloserFont.title1)
Text("Your choices are recorded. See how they match with your partner when they play too.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
Button("Play Again") {
currentPair = 0
choices = []
showResults = false
}
.buttonStyle(PrimaryButtonStyle())
}
} else {
// Progress
Text("\(currentPair + 1) of \(pairs.count)")
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
ProgressView(value: Double(currentPair + 1), total: Double(pairs.count))
.tint(.closerPrimary)
// Current pair
VStack(spacing: CloserSpacing.lg) {
Text("Would you rather...")
.font(CloserFont.title3)
.foregroundColor(.closerText)
let options = [pairs[currentPair].0, pairs[currentPair].1]
ForEach(options, id: \.self) { option in
Button(action: { choose(option) }) {
Text(option)
.font(CloserFont.body)
.foregroundColor(.closerText)
.frame(maxWidth: .infinity)
.padding()
.background(Color.closerSurface)
.cornerRadius(CloserRadius.large)
.overlay(RoundedRectangle(cornerRadius: CloserRadius.large).stroke(Color.closerDivider))
}
}
}
}
}
.closerPadding()
.background(Color.closerBackground)
.navigationTitle("This or That")
.navigationBarTitleDisplayMode(.inline)
}
private func choose(_ option: String) {
choices.append(option)
if currentPair < pairs.count - 1 {
withAnimation {
currentPair += 1
}
} else {
withAnimation {
showResults = true
}
}
}
}
// MARK: - How Well Do You Know Me
struct HowWellView: View {
@State private var currentQuestion = 0
@State private var score = 0
@State private var showResults = false
@State private var selectedAnswer: String?
let questions: [(question: String, options: [String], correctIndex: Int)] = [
("What's my favorite color?", ["Blue", "Red", "Green", "Purple"], 0),
("What's my go-to comfort food?", ["Pizza", "Ice cream", "Pasta", "Chocolate"], 1),
("What would I do with a free day?", ["Read a book", "Go outside", "Watch movies", "Sleep in"], 2),
("What's my dream travel destination?", ["Japan", "Italy", "New Zealand", "Greece"], 3),
("Am I more introverted or extroverted?", ["Introverted", "Extroverted", "It depends", "Both equally"], 0),
]
var body: some View {
VStack(spacing: CloserSpacing.xxl) {
if showResults {
VStack(spacing: CloserSpacing.lg) {
Image(systemName: score == questions.count ? "star.fill" : "heart.fill")
.font(.system(size: 64))
.foregroundColor(score == questions.count ? .closerGold : .closerPrimary)
Text("\(score) / \(questions.count)")
.font(CloserFont.title1)
Text(scoreMessage)
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
Button("Play Again") {
currentQuestion = 0
score = 0
showResults = false
}
.buttonStyle(PrimaryButtonStyle())
}
} else {
Text("\(currentQuestion + 1) of \(questions.count)")
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
ProgressView(value: Double(currentQuestion + 1), total: Double(questions.count))
.tint(.closerPrimary)
Text(questions[currentQuestion].question)
.font(CloserFont.title2)
.foregroundColor(.closerText)
.multilineTextAlignment(.center)
VStack(spacing: CloserSpacing.md) {
ForEach(questions[currentQuestion].options.indices, id: \.self) { index in
Button(action: { selectAnswer(index) }) {
Text(questions[currentQuestion].options[index])
.font(CloserFont.body)
.foregroundColor(.closerText)
.frame(maxWidth: .infinity)
.padding()
.background(selectedAnswer == questions[currentQuestion].options[index] ? Color.closerPrimary.opacity(0.1) : Color.closerSurface)
.cornerRadius(CloserRadius.large)
.overlay(RoundedRectangle(cornerRadius: CloserRadius.large).stroke(
selectedAnswer == questions[currentQuestion].options[index] ? Color.closerPrimary : Color.closerDivider
))
}
}
}
}
}
.closerPadding()
.background(Color.closerBackground)
.navigationTitle("How Well Do You Know Me")
.navigationBarTitleDisplayMode(.inline)
}
private func selectAnswer(_ index: Int) {
let correct = questions[currentQuestion].correctIndex
if index == correct { score += 1 }
selectedAnswer = questions[currentQuestion].options[index]
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
selectedAnswer = nil
if currentQuestion < questions.count - 1 {
withAnimation { currentQuestion += 1 }
} else {
withAnimation { showResults = true }
}
}
}
private var scoreMessage: String {
switch score {
case 0...2: return "Time to learn more about each other!"
case 3...4: return "You know your partner pretty well!"
case 5: return "Perfect score! You really know each other!"
default: return ""
}
}
}
// MARK: - Desire Sync
struct DesireSyncView: View {
@State private var currentQuestion = 0
@State private var preferences: [String: Int] = [:]
@State private var showResults = false
let questions: [(question: String, item: String)] = [
("How important is regular date night?", "date_night"),
("How important is daily check-in?", "daily_checkin"),
("How important is physical intimacy?", "intimacy"),
("How important is shared adventure?", "adventure"),
("How important is quality time at home?", "home_time"),
]
var body: some View {
VStack(spacing: CloserSpacing.xxl) {
if showResults {
VStack(spacing: CloserSpacing.lg) {
Image(systemName: "sparkles")
.font(.system(size: 64))
.foregroundColor(.closerPrimary)
Text("Preferences Recorded!")
.font(CloserFont.title1)
Text("Your responses are saved. Compare with your partner when they complete theirs.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
Button("View Comparison") {
// Navigate to comparison view
}
.buttonStyle(PrimaryButtonStyle())
Button("Play Again") {
currentQuestion = 0
preferences = [:]
showResults = false
}
.buttonStyle(SecondaryButtonStyle())
}
} else {
Text("\(currentQuestion + 1) of \(questions.count)")
.font(CloserFont.subheadline)
.foregroundColor(.closerTextSecondary)
ProgressView(value: Double(currentQuestion + 1), total: Double(questions.count))
.tint(.closerPrimary)
Text(questions[currentQuestion].question)
.font(CloserFont.title2)
.foregroundColor(.closerText)
.multilineTextAlignment(.center)
VStack(spacing: CloserSpacing.sm) {
ForEach(1...5, id: \.self) { value in
Button(action: { setPreference(value) }) {
HStack {
Text(desireLabel(value))
.font(CloserFont.callout)
.foregroundColor(.closerText)
Spacer()
if preferences[questions[currentQuestion].item] == value {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.closerPrimary)
}
}
.padding()
.background(preferences[questions[currentQuestion].item] == value ? Color.closerPrimary.opacity(0.1) : Color.closerSurface)
.cornerRadius(CloserRadius.medium)
.overlay(RoundedRectangle(cornerRadius: CloserRadius.medium).stroke(Color.closerDivider))
}
}
}
}
}
.closerPadding()
.background(Color.closerBackground)
.navigationTitle("Desire Sync")
.navigationBarTitleDisplayMode(.inline)
}
private func setPreference(_ value: Int) {
preferences[questions[currentQuestion].item] = value
if currentQuestion < questions.count - 1 {
withAnimation { currentQuestion += 1 }
} else {
withAnimation { showResults = true }
}
}
private func desireLabel(_ value: Int) -> String {
switch value {
case 1: return "Not important"
case 2: return "Slightly important"
case 3: return "Moderately important"
case 4: return "Very important"
case 5: return "Essential"
default: return ""
}
}
}
// MARK: - Connection Challenges
struct ConnectionChallengesView: View {
@State private var challenges: [ConnectionChallenge] = []
@State private var isLoading = true
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: CloserSpacing.lg) {
Text("Connection Challenges")
.font(CloserFont.title1)
.foregroundColor(.closerText)
.closerPadding()
Text("Multi-day programs designed to strengthen your bond")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.closerPadding()
if isLoading {
LoadingView(message: "Loading challenges...")
} else if challenges.isEmpty {
EmptyStateView(
icon: "mountain.2",
title: "No Challenges Yet",
message: "Connection challenges are coming soon!"
)
} else {
ForEach(challenges) { challenge in
ChallengeCard(challenge: challenge)
.closerPadding()
}
}
}
.padding(.vertical)
}
.background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline)
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
}
struct ChallengeCard: View {
let challenge: ConnectionChallenge
var body: some View {
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
HStack {
Text(challenge.title)
.font(CloserFont.headline)
.foregroundColor(.closerText)
Spacer()
if challenge.isPremium {
PremiumBadge()
}
}
Text(challenge.description)
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
HStack {
Image(systemName: "calendar")
Text("\(challenge.durationDays) days")
}
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
}
.padding(CloserSpacing.md)
.closerCard()
}
}
// MARK: - Memory Lane
struct MemoryLaneView: View {
@State private var capsules: [MemoryCapsule] = []
@State private var isLoading = true
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: CloserSpacing.lg) {
Text("Memory Lane")
.font(CloserFont.title1)
.foregroundColor(.closerText)
.closerPadding()
if isLoading {
LoadingView(message: "Loading memories...")
} else if capsules.isEmpty {
EmptyStateView(
icon: "clock.fill",
title: "No Memories Yet",
message: "Create time capsules to unlock memories in the future."
)
} else {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: CloserSpacing.md) {
ForEach(capsules) { capsule in
CapsuleCard(capsule: capsule)
}
}
.closerPadding()
}
}
.padding(.vertical)
}
.background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline)
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
}
struct CapsuleCard: View {
let capsule: MemoryCapsule
var body: some View {
VStack(spacing: CloserSpacing.sm) {
Image(systemName: capsule.status == "unlocked" ? "envelope.open.fill" : "envelope.fill")
.font(.title)
.foregroundColor(capsule.status == "unlocked" ? .closerPrimary : .closerTextSecondary)
Text(capsule.title)
.font(CloserFont.headline)
.foregroundColor(.closerText)
.lineLimit(2)
if capsule.status == "sealed" {
Text("Unlocks \(capsule.unlockAt, style: .date)")
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
}
if capsule.status == "unlocked" {
Text("Open")
.font(CloserFont.caption)
.foregroundColor(.closerSuccess)
}
}
.padding(CloserSpacing.md)
.frame(maxWidth: .infinity)
.closerCard()
}
}
// MARK: - Waiting for Partner
struct WaitingForPartnerView: View {
let gameName: String
var body: some View {
VStack(spacing: CloserSpacing.xxl) {
Image(systemName: "hourglass")
.font(.system(size: 64))
.foregroundColor(.closerPrimary)
Text("Waiting for \(gameName)")
.font(CloserFont.title2)
.foregroundColor(.closerText)
Text("Your partner hasn't finished this activity yet. Results will appear here once they do.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
ProgressView()
.tint(.closerPrimary)
.scaleEffect(1.5)
}
.closerPadding()
.background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline)
}
}
// MARK: - Replay Views
struct ThisOrThatReplayView: View {
let sessionId: String
var body: some View {
WaitingForPartnerView(gameName: "This or That")
.navigationTitle("Results")
.navigationBarTitleDisplayMode(.inline)
}
}
struct HowWellReplayView: View {
let sessionId: String
var body: some View {
WaitingForPartnerView(gameName: "How Well Do You Know Me")
.navigationTitle("Results")
.navigationBarTitleDisplayMode(.inline)
}
}
struct DesireSyncReplayView: View {
let sessionId: String
var body: some View {
WaitingForPartnerView(gameName: "Desire Sync")
.navigationTitle("Results")
.navigationBarTitleDisplayMode(.inline)
}
}
// MARK: - Helper Extensions
extension String {
func replacing(_ target: String, with replacement: String) -> String {
replacingOccurrences(of: target, with: replacement)
}
}