// // AddMoodHeaderView.swift // Feels // // Created by Trey Tartt on 1/5/22. // import Foundation import SwiftUI import SwiftData struct AddMoodHeaderView: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @State var onboardingData = OnboardingDataDataManager.shared.savedOnboardingData // Celebration animation state @State private var showCelebration = false @State private var celebrationMood: Mood = .great @State private var celebrationAnimation: CelebrationAnimationType = .pulseWave @State private var celebrationDate: Date = Date() let addItemHeaderClosure: ((Mood, Date) -> Void) init(addItemHeaderClosure: @escaping ((Mood, Date) -> Void)) { self.addItemHeaderClosure = addItemHeaderClosure } private var layoutStyle: VotingLayoutStyle { VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal } var body: some View { ZStack { // Force re-render when image pack changes Text(String(imagePack.rawValue)) .hidden() theme.currentTheme.secondaryBGColor VStack(spacing: 16) { Text(ShowBasedOnVoteLogics.getVotingTitle(onboardingData: onboardingData)) .font(.title2.bold()) .foregroundColor(textColor) .padding(.top) votingLayoutContent .padding(.bottom) } .padding(.horizontal) .opacity(showCelebration ? 0 : 1) // Celebration animation overlay if showCelebration { CelebrationOverlayView( animationType: celebrationAnimation, mood: celebrationMood ) { // Animation complete - save the mood (parent will remove this view) addItemHeaderClosure(celebrationMood, celebrationDate) } } } .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .fixedSize(horizontal: false, vertical: true) } @ViewBuilder private var votingLayoutContent: some View { switch layoutStyle { case .horizontal: HorizontalVotingView(moodTint: moodTint, onMoodSelected: addItem) case .cards: CardVotingView(moodTint: moodTint, onMoodSelected: addItem) case .radial: RadialVotingView(moodTint: moodTint, onMoodSelected: addItem) case .stacked: StackedVotingView(moodTint: moodTint, onMoodSelected: addItem) case .aura: AuraVotingView(moodTint: moodTint, onMoodSelected: addItem) case .orbit: OrbitVotingView(moodTint: moodTint, onMoodSelected: addItem) } } private func addItem(withMood mood: Mood) { let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() // Store mood, date, and pick random animation celebrationMood = mood celebrationDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData) celebrationAnimation = .random // Show celebration - mood will be saved when animation completes withAnimation(.easeInOut(duration: 0.3)) { showCelebration = true } } } // MARK: - Layout 1: Horizontal (Polished version of current) struct HorizontalVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void var body: some View { HStack(spacing: 8) { ForEach(Mood.allValues) { mood in Button(action: { onMoodSelected(mood) }) { mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 55, height: 55) .foregroundColor(moodTint.color(forMood: mood)) } .buttonStyle(MoodButtonStyle()) .frame(maxWidth: .infinity) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } } // MARK: - Layout 2: Cards Grid struct CardVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void private let columns = [ GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12) ] var body: some View { LazyVGrid(columns: columns, spacing: 12) { ForEach(Mood.allValues) { mood in Button(action: { onMoodSelected(mood) }) { VStack(spacing: 8) { mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) .foregroundColor(moodTint.color(forMood: mood)) Text(mood.strValue) .font(.caption.weight(.medium)) .foregroundColor(moodTint.color(forMood: mood)) } .frame(maxWidth: .infinity) .padding(.vertical, 16) .background( RoundedRectangle(cornerRadius: 12) .fill(moodTint.color(forMood: mood).opacity(0.15)) ) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(moodTint.color(forMood: mood).opacity(0.3), lineWidth: 1) ) } .buttonStyle(CardButtonStyle()) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } } // MARK: - Layout 3: Radial/Semi-circle struct RadialVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void var body: some View { GeometryReader { geometry in let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height * 0.9) let radius = min(geometry.size.width, geometry.size.height) * 0.65 let moods = Mood.allValues ZStack { ForEach(Array(moods.enumerated()), id: \.element.id) { index, mood in let angle = angleForIndex(index, total: moods.count) let position = positionForAngle(angle, radius: radius, center: center) Button(action: { onMoodSelected(mood) }) { VStack(spacing: 4) { mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 44, height: 44) .foregroundColor(moodTint.color(forMood: mood)) Text(mood.strValue) .font(.caption2.weight(.medium)) .foregroundColor(moodTint.color(forMood: mood)) } .padding(8) .background( Circle() .fill(moodTint.color(forMood: mood).opacity(0.1)) ) } .buttonStyle(MoodButtonStyle()) .position(position) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } } .frame(height: 180) .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } private func angleForIndex(_ index: Int, total: Int) -> Double { // Spread moods across a semi-circle (180 degrees), from left to right let startAngle = Double.pi // 180 degrees (left) let endAngle = 0.0 // 0 degrees (right) let step = (startAngle - endAngle) / Double(total - 1) return startAngle - (step * Double(index)) } private func positionForAngle(_ angle: Double, radius: CGFloat, center: CGPoint) -> CGPoint { CGPoint( x: center.x + radius * CGFloat(cos(angle)), y: center.y - radius * CGFloat(sin(angle)) ) } } // MARK: - Layout 4: Stacked Full-width struct StackedVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void var body: some View { VStack(spacing: 10) { ForEach(Mood.allValues) { mood in Button(action: { onMoodSelected(mood) }) { HStack(spacing: 16) { mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 36, height: 36) .foregroundColor(moodTint.color(forMood: mood)) Text(mood.strValue) .font(.body.weight(.semibold)) .foregroundColor(moodTint.color(forMood: mood)) Spacer() Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundColor(moodTint.color(forMood: mood).opacity(0.5)) } .padding(.horizontal, 16) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 12) .fill(moodTint.color(forMood: mood).opacity(0.12)) ) } .buttonStyle(CardButtonStyle()) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } } // MARK: - Layout 5: Aura (Atmospheric glowing orbs) struct AuraVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: 24) { // Top row: 3 moods (Horrible, Bad, Average) HStack(spacing: 16) { ForEach(Array(Mood.allValues.prefix(3))) { mood in auraButton(for: mood) } } // Bottom row: 2 moods (Good, Great) - centered HStack(spacing: 24) { ForEach(Array(Mood.allValues.suffix(2))) { mood in auraButton(for: mood) } } } .padding(.vertical, 8) } private func auraButton(for mood: Mood) -> some View { let color = moodTint.color(forMood: mood) return Button(action: { onMoodSelected(mood) }) { VStack(spacing: 10) { // Glowing orb ZStack { // Outer atmospheric glow Circle() .fill( RadialGradient( colors: [ color.opacity(0.5), color.opacity(0.2), Color.clear ], center: .center, startRadius: 0, endRadius: 45 ) ) .frame(width: 90, height: 90) // Middle glow ring Circle() .fill( RadialGradient( colors: [ color.opacity(0.8), color.opacity(0.4) ], center: .center, startRadius: 10, endRadius: 30 ) ) .frame(width: 60, height: 60) // Inner solid core Circle() .fill(color) .frame(width: 48, height: 48) .shadow(color: color.opacity(0.8), radius: 12, x: 0, y: 0) // Icon mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 26, height: 26) .foregroundColor(.white) } // Label with elegant typography Text(mood.strValue) .font(.caption.weight(.semibold)) .foregroundColor(color) .tracking(0.5) } } .buttonStyle(AuraButtonStyle(color: color)) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } // MARK: - Layout 6: Orbit (Celestial circular arrangement) struct OrbitVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void @Environment(\.colorScheme) private var colorScheme @State private var centerPulse: CGFloat = 1.0 private var isDark: Bool { colorScheme == .dark } var body: some View { GeometryReader { geo in let size = min(geo.size.width, geo.size.height) let centerX = geo.size.width / 2 let centerY = size / 2 let orbitRadius = size * 0.38 ZStack { orbitalRing(radius: orbitRadius, centerX: centerX, centerY: centerY) centerCore(centerX: centerX, centerY: centerY) moodPlanets(radius: orbitRadius, centerX: centerX, centerY: centerY) } } .frame(height: 260) .onAppear { withAnimation(.easeInOut(duration: 2.0).repeatForever(autoreverses: true)) { centerPulse = 1.1 } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } private func orbitalRing(radius: CGFloat, centerX: CGFloat, centerY: CGFloat) -> some View { let ringColor = isDark ? Color.white : Color.black return Circle() .stroke(ringColor.opacity(0.08), lineWidth: 1) .frame(width: radius * 2, height: radius * 2) .position(x: centerX, y: centerY) } private func centerCore(centerX: CGFloat, centerY: CGFloat) -> some View { let glowOpacity = isDark ? 0.15 : 0.3 let coreOpacity = isDark ? 0.9 : 1.0 return ZStack { Circle() .fill(Color.white.opacity(glowOpacity)) .frame(width: 80, height: 80) .scaleEffect(centerPulse) Circle() .fill(Color.white.opacity(coreOpacity)) .frame(width: 36, height: 36) .shadow(color: .white.opacity(0.5), radius: 10) } .position(x: centerX, y: centerY) } private func moodPlanets(radius: CGFloat, centerX: CGFloat, centerY: CGFloat) -> some View { ForEach(Array(Mood.allValues.enumerated()), id: \.element.id) { index, mood in orbitMoodButton( for: mood, index: index, radius: radius, centerX: centerX, centerY: centerY ) } } private func orbitMoodButton(for mood: Mood, index: Int, radius: CGFloat, centerX: CGFloat, centerY: CGFloat) -> some View { let angle = -Double.pi / 2 + (2 * Double.pi / 5) * Double(index) let posX = centerX + cos(angle) * radius let posY = centerY + sin(angle) * radius let color = moodTint.color(forMood: mood) return Button(action: { onMoodSelected(mood) }) { OrbitMoodButtonContent(mood: mood, color: color) } .buttonStyle(OrbitButtonStyle(color: color)) .position(x: posX, y: posY) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } struct OrbitMoodButtonContent: View { let mood: Mood let color: Color var body: some View { ZStack { Circle() .fill(color.opacity(0.2)) .frame(width: 70, height: 70) Circle() .fill(color) .frame(width: 52, height: 52) .shadow(color: color.opacity(0.6), radius: 8, x: 0, y: 2) mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(.white) } } } // Button style for orbit moods struct OrbitButtonStyle: ButtonStyle { let color: Color @Environment(\.accessibilityReduceMotion) private var reduceMotion func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 1.15 : 1.0) .shadow( color: configuration.isPressed ? color.opacity(0.8) : Color.clear, radius: configuration.isPressed ? 20 : 0, x: 0, y: 0 ) .animation(reduceMotion ? nil : .spring(response: 0.3, dampingFraction: 0.6), value: configuration.isPressed) } } // Custom button style for aura with glow effect on press struct AuraButtonStyle: ButtonStyle { let color: Color @Environment(\.accessibilityReduceMotion) private var reduceMotion func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.92 : 1.0) .brightness(configuration.isPressed ? 0.1 : 0) .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: configuration.isPressed) } } // MARK: - Button Styles struct MoodButtonStyle: ButtonStyle { @Environment(\.accessibilityReduceMotion) private var reduceMotion func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.9 : 1.0) .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: configuration.isPressed) } } struct CardButtonStyle: ButtonStyle { @Environment(\.accessibilityReduceMotion) private var reduceMotion func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.96 : 1.0) .opacity(configuration.isPressed ? 0.8 : 1.0) .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: configuration.isPressed) } } // MARK: - Previews struct AddMoodHeaderView_Previews: PreviewProvider { static var previews: some View { Group { AddMoodHeaderView(addItemHeaderClosure: { (_,_) in }).modelContainer(DataController.shared.container) AddMoodHeaderView(addItemHeaderClosure: { (_,_) in }).preferredColorScheme(.dark).modelContainer(DataController.shared.container) } } }