337 lines
12 KiB
Swift
337 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Category Picker
|
|
|
|
struct CategoryPickerView: View {
|
|
@State private var selectedCategory: String?
|
|
|
|
let categories: [(name: String, icon: String, color: Color)] = [
|
|
("Communication", "bubble.left.and.bubble.right.fill", .categoryCommunication),
|
|
("Intimacy", "heart.fill", .categoryIntimacy),
|
|
("Fun", "gamecontroller.fill", .categoryFun),
|
|
("Goals", "target", .categoryGoals),
|
|
("Adventure", "paperplane.fill", .categoryAdventure),
|
|
("Romance", "sparkles", .closerSecondary),
|
|
("Deep", "brain.head.profile", .closerPrimary),
|
|
("Random", "shuffle", .closerTextSecondary),
|
|
]
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: CloserSpacing.xl) {
|
|
Text("Choose a Category")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
.closerPadding()
|
|
|
|
Text("Pick a topic and spin the wheel!")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.closerPadding()
|
|
|
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: CloserSpacing.md) {
|
|
ForEach(categories, id: \.name) { category in
|
|
Button(action: { selectedCategory = category.name }) {
|
|
VStack(spacing: CloserSpacing.sm) {
|
|
CategoryGlyph(name: category.name, color: category.color, isLarge: true)
|
|
Text(category.name)
|
|
.font(CloserFont.headline)
|
|
.foregroundColor(.closerText)
|
|
}
|
|
.padding(CloserSpacing.md)
|
|
.frame(maxWidth: .infinity)
|
|
.closerCard()
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.closerPadding()
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationTitle("Spin the Wheel")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationDestination(item: $selectedCategory) { category in
|
|
SpinWheelView(category: category)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Spin Wheel
|
|
|
|
struct SpinWheelView: View {
|
|
let category: String
|
|
@State private var rotation: Double = 0
|
|
@State private var isSpinning = false
|
|
@State private var selectedSlice: Int?
|
|
@State private var showResult = false
|
|
@State private var navigateToSession = false
|
|
|
|
let slices: [(label: String, color: Color)] = [
|
|
("Question", .closerPrimary),
|
|
("Challenge", .closerSecondary),
|
|
("Compliment", .categoryCommunication),
|
|
("Share", .categoryGoals),
|
|
("Date", .categoryAdventure),
|
|
("Story", .categoryFun),
|
|
("Memory", .closerGold),
|
|
("Dream", .categoryIntimacy),
|
|
]
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xl) {
|
|
Text("Category: \(category)")
|
|
.font(CloserFont.title2)
|
|
.foregroundColor(.closerText)
|
|
|
|
// Wheel
|
|
ZStack {
|
|
Image("illustration-spin-wheel")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 320, height: 320)
|
|
.rotationEffect(.degrees(rotation))
|
|
|
|
// Center circle
|
|
Circle()
|
|
.fill(Color.closerBackground)
|
|
.frame(width: 60, height: 60)
|
|
.closerShadow(level: .medium)
|
|
|
|
Circle()
|
|
.stroke(Color.closerSurface.opacity(0.9), lineWidth: 8)
|
|
.frame(width: 320, height: 320)
|
|
|
|
// Pointer (top)
|
|
Image(systemName: "arrowtriangle.down.fill")
|
|
.font(.title)
|
|
.foregroundColor(.closerPrimary)
|
|
.offset(y: -160)
|
|
}
|
|
.frame(width: 320, height: 320)
|
|
.animation(isSpinning ? .spring(response: 1.5, dampingFraction: 0.6) : .default, value: rotation)
|
|
|
|
// Result
|
|
if let slice = selectedSlice, showResult {
|
|
VStack(spacing: CloserSpacing.sm) {
|
|
Text("You got:")
|
|
.font(CloserFont.subheadline)
|
|
.foregroundColor(.closerTextSecondary)
|
|
Text(slices[slice].label)
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(slices[slice].color)
|
|
}
|
|
.transition(.scale.combined(with: .opacity))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Action buttons
|
|
VStack(spacing: CloserSpacing.md) {
|
|
Button(action: spin) {
|
|
HStack {
|
|
Image(systemName: "arrow.triangle.2.circlepath")
|
|
Text(isSpinning ? "Spinning..." : "Spin!")
|
|
}
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning))
|
|
.disabled(isSpinning)
|
|
|
|
if showResult {
|
|
Button("Continue") {
|
|
navigateToSession = true
|
|
}
|
|
.buttonStyle(SecondaryButtonStyle())
|
|
}
|
|
}
|
|
.closerPadding()
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationDestination(isPresented: $navigateToSession) {
|
|
WheelSessionView(sessionId: UUID().uuidString, category: category, slice: slices[selectedSlice ?? 0].label)
|
|
}
|
|
}
|
|
|
|
private func spin() {
|
|
isSpinning = true
|
|
showResult = false
|
|
selectedSlice = nil
|
|
|
|
let randomSpin = Double.random(in: 1080...3600) // 3-10 full rotations
|
|
let target = randomSpin
|
|
|
|
withAnimation(.spring(response: 1.5, dampingFraction: 0.5)) {
|
|
rotation += target
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
|
|
// Calculate which slice the pointer is on
|
|
let sliceAngle = 360.0 / Double(slices.count)
|
|
let normalizedRotation = rotation.truncatingRemainder(dividingBy: 360)
|
|
let pointerAngle = (360 - normalizedRotation).truncatingRemainder(dividingBy: 360)
|
|
let index = Int(pointerAngle / sliceAngle)
|
|
|
|
selectedSlice = min(max(index, 0), slices.count - 1)
|
|
showResult = true
|
|
isSpinning = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Wheel Session
|
|
|
|
struct WheelSessionView: View {
|
|
let sessionId: String
|
|
let category: String
|
|
let slice: String
|
|
@State private var showComplete = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
Image(systemName: iconForSlice(slice))
|
|
.font(.system(size: 64))
|
|
.foregroundColor(colorForSlice(slice))
|
|
|
|
Text("Your \(category) \(slice.lowercased())")
|
|
.font(CloserFont.title2)
|
|
.foregroundColor(.closerText)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(promptFor(category: category, slice: slice))
|
|
.font(CloserFont.body)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.closerPadding()
|
|
|
|
Spacer()
|
|
|
|
Button("Complete") {
|
|
showComplete = true
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationDestination(isPresented: $showComplete) {
|
|
WheelCompleteView(sessionId: sessionId)
|
|
}
|
|
}
|
|
|
|
private func iconForSlice(_ slice: String) -> String {
|
|
switch slice {
|
|
case "Question": return "questionmark.bubble.fill"
|
|
case "Challenge": return "mountain.2.fill"
|
|
case "Compliment": return "hand.thumbsup.fill"
|
|
case "Share": return "square.and.arrow.up.fill"
|
|
case "Date": return "heart.fill"
|
|
case "Story": return "book.fill"
|
|
case "Memory": return "clock.fill"
|
|
case "Dream": return "sparkles"
|
|
default: return "star.fill"
|
|
}
|
|
}
|
|
|
|
private func colorForSlice(_ slice: String) -> Color {
|
|
switch slice {
|
|
case "Question": return .closerPrimary
|
|
case "Challenge": return .closerSecondary
|
|
case "Compliment": return .categoryCommunication
|
|
case "Share": return .categoryGoals
|
|
case "Date": return .categoryAdventure
|
|
case "Story": return .categoryFun
|
|
case "Memory": return .closerGold
|
|
case "Dream": return .categoryIntimacy
|
|
default: return .closerPrimary
|
|
}
|
|
}
|
|
|
|
private func promptFor(category: String, slice: String) -> String {
|
|
// This would pull from a database of category+slice prompts
|
|
return "Take a moment to share something meaningful with your partner about \(category.lowercased()) in the form of a \(slice.lowercased())."
|
|
}
|
|
}
|
|
|
|
// MARK: - Wheel Complete
|
|
|
|
struct WheelCompleteView: View {
|
|
let sessionId: String
|
|
@EnvironmentObject var appState: AppState
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
HeartBurstView()
|
|
|
|
Text("Session Complete!")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
|
|
Text("Great job connecting with your partner. Every moment together makes you stronger.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
NavigationLink {
|
|
WheelHistoryView()
|
|
} label: {
|
|
Label("View History", systemImage: "clock.arrow.circlepath")
|
|
}
|
|
.buttonStyle(SecondaryButtonStyle())
|
|
|
|
Button("Back to Play") {
|
|
// Pop to root
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarBackButtonHidden()
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|
|
|
|
// MARK: - Wheel History
|
|
|
|
struct WheelHistoryView: 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 Wheel Sessions Yet",
|
|
message: "Spin the wheel to start building your history!"
|
|
)
|
|
.listRowBackground(Color.clear)
|
|
} else {
|
|
ForEach(sessions) { session in
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Wheel Session")
|
|
.font(CloserFont.body)
|
|
.foregroundColor(.closerText)
|
|
Text(session.startedAt, style: .date)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerTextSecondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.background(Color.closerBackground)
|
|
.navigationTitle("Wheel History")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task {
|
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|