// // AddMoodHeaderView.swift // Reflect // // 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.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0 @AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true private var textColor: Color { theme.currentTheme.labelColor } @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() 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) } } } .background(theme.currentTheme.secondaryBGColor) .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 .stacked: StackedVotingView(moodTint: moodTint, onMoodSelected: addItem) case .aura: AuraVotingView(moodTint: moodTint, onMoodSelected: addItem) case .orbit: OrbitVotingView(moodTint: moodTint, onMoodSelected: addItem) case .neon: NeonVotingView(moodTint: moodTint, onMoodSelected: addItem) } } private func addItem(withMood mood: Mood) { // Store mood, date, and use saved animation preference celebrationMood = mood celebrationDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData) celebrationAnimation = CelebrationAnimationType.fromIndex(celebrationAnimationIndex) // Play haptic feedback matching the selected animation if hapticFeedbackEnabled { HapticFeedbackManager.shared.play(for: celebrationAnimation) } // 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) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.isButton) .accessibilityIdentifier(AccessibilityID.MoodButton.id(for: mood.widgetDisplayName)) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } } } // MARK: - Layout 2: Cards Grid struct CardVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void var body: some View { GeometryReader { geo in let spacing: CGFloat = 12 let cardWidth = (geo.size.width - spacing * 2) / 3 // Offset to center bottom row cards between top row cards // Each bottom card should be centered between two top cards let bottomOffset = (cardWidth + spacing) / 2 VStack(spacing: spacing) { // Top row: Great, Good, Average HStack(spacing: spacing) { ForEach(Array(Mood.allValues.prefix(3))) { mood in cardButton(for: mood, width: cardWidth) } } // Bottom row: Bad, Horrible - centered between top row items HStack(spacing: spacing) { ForEach(Array(Mood.allValues.suffix(2))) { mood in cardButton(for: mood, width: cardWidth) } } .padding(.leading, bottomOffset) .padding(.trailing, bottomOffset) } } .frame(height: 190) } private func cardButton(for mood: Mood, width: CGFloat) -> some View { Button(action: { onMoodSelected(mood) }) { mood.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) .foregroundColor(moodTint.color(forMood: mood)) .frame(width: width) .padding(.vertical, 20) .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()) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.isButton) .accessibilityIdentifier(AccessibilityID.MoodButton.id(for: mood.widgetDisplayName)) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } // MARK: - Layout 3: 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()) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.isButton) .accessibilityIdentifier(AccessibilityID.MoodButton.id(for: mood.widgetDisplayName)) .accessibilityLabel(mood.strValue) .accessibilityHint(String(localized: "Select this mood")) } } } } // 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) }) { // 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) } } .buttonStyle(AuraButtonStyle(color: color)) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.isButton) .accessibilityIdentifier(AccessibilityID.MoodButton.id(for: mood.widgetDisplayName)) .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 } } .onDisappear { centerPulse = 1.0 } } 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) .accessibilityElement(children: .ignore) .accessibilityAddTraits(.isButton) .accessibilityIdentifier(AccessibilityID.MoodButton.id(for: mood.widgetDisplayName)) .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) } } } // MARK: - Layout 7: Neon (Synthwave Arcade Equalizer) struct NeonVotingView: View { let moodTint: MoodTints let onMoodSelected: (Mood) -> Void @State private var pulsePhase = false @State private var hoveredMood: Mood? // Synthwave color palette private let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82) private let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8) private let neonYellow = Color(red: 1.0, green: 0.9, blue: 0.0) private let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04) var body: some View { ZStack { // Grid background neonGridBackground // Equalizer bars HStack(spacing: 8) { ForEach(Mood.allValues, id: \.self) { mood in NeonEqualizerBar( mood: mood, moodTint: moodTint, isHovered: hoveredMood == mood, pulsePhase: pulsePhase, neonCyan: neonCyan, neonMagenta: neonMagenta, onTap: { onMoodSelected(mood) } ) } } .padding(.horizontal, 16) .padding(.vertical, 20) } .frame(height: 200) .clipShape(RoundedRectangle(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke( LinearGradient( colors: [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) ) .shadow(color: neonCyan.opacity(0.2), radius: 20, x: 0, y: 0) .shadow(color: neonMagenta.opacity(0.15), radius: 30, x: 0, y: 10) .onAppear { withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { pulsePhase = true } } .onDisappear { pulsePhase = false } } private var neonGridBackground: some View { ZStack { // Deep black base deepBlack // Grid Canvas { context, size in let gridSpacing: CGFloat = 20 let cyanColor = Color(red: 0.0, green: 0.8, blue: 0.7) // Horizontal lines for y in stride(from: 0, to: size.height, by: gridSpacing) { var path = Path() path.move(to: CGPoint(x: 0, y: y)) path.addLine(to: CGPoint(x: size.width, y: y)) context.stroke(path, with: .color(cyanColor.opacity(0.08)), lineWidth: 0.5) } // Vertical lines for x in stride(from: 0, to: size.width, by: gridSpacing) { var path = Path() path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x, y: size.height)) context.stroke(path, with: .color(cyanColor.opacity(0.08)), lineWidth: 0.5) } } // Ambient glow at bottom LinearGradient( colors: [ neonMagenta.opacity(0.15), Color.clear ], startPoint: .bottom, endPoint: .top ) .blur(radius: 30) } } } struct NeonEqualizerBar: View { let mood: Mood let moodTint: MoodTints let isHovered: Bool let pulsePhase: Bool let neonCyan: Color let neonMagenta: Color let onTap: () -> Void @State private var isPressed = false private var barHeight: CGFloat { switch mood { case .great: return 140 case .good: return 115 case .average: return 90 case .bad: return 65 case .horrible: return 45 default: return 90 } } private var barColor: Color { switch mood { case .great: return neonCyan case .good: return Color(red: 0.2, green: 1.0, blue: 0.6) case .average: return Color(red: 1.0, green: 0.9, blue: 0.0) case .bad: return Color(red: 1.0, green: 0.5, blue: 0.0) case .horrible: return neonMagenta default: return Color(red: 1.0, green: 0.9, blue: 0.0) } } var body: some View { Button(action: onTap) { // The equalizer bar ZStack(alignment: .bottom) { // Glow background RoundedRectangle(cornerRadius: 6) .fill(barColor.opacity(pulsePhase ? 0.15 : 0.08)) .frame(height: barHeight + 20) .blur(radius: 15) // Main bar RoundedRectangle(cornerRadius: 6) .fill( LinearGradient( colors: [ barColor, barColor.opacity(0.7) ], startPoint: .top, endPoint: .bottom ) ) .frame(height: isPressed ? barHeight * 0.9 : barHeight) .shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0) .shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5) // Top highlight RoundedRectangle(cornerRadius: 6) .fill( LinearGradient( colors: [Color.white.opacity(0.5), Color.clear], startPoint: .top, endPoint: .center ) ) .frame(height: isPressed ? barHeight * 0.9 : barHeight) // Level indicators (horizontal lines) VStack(spacing: 8) { ForEach(0.. some View { configuration.label .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) .onChange(of: configuration.isPressed) { _, newValue in isPressed = newValue } } }