Closer/iphone/Closer/Wheel/WheelViews.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
}
}
}