diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index 454c8e1..2a44f93 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -85,91 +85,194 @@ struct MainTabView: View { } } -// MARK: - Success Confetti Animation +// MARK: - Success Confetti Animation (canvas-confetti style) struct SuccessConfettiView: View { - @State private var particles: [ConfettiParticle] = [] + @State private var particles: [ConfettiPiece] = [] @State private var checkScale: CGFloat = 0 + @State private var checkOpacity: Double = 0 - struct ConfettiParticle: Identifiable { + struct ConfettiPiece: Identifiable { let id = UUID() - let emoji: String + let color: Color + let size: CGFloat let x: CGFloat + let startY: CGFloat + let endY: CGFloat + let drift: CGFloat let delay: Double let duration: Double - let rotation: Double + let spin: Double + let shape: Int // 0=circle, 1=rect, 2=star } + static let colors: [Color] = [ + Color(hex: "FF6B6B"), Color(hex: "4ECDC4"), Color(hex: "FFE66D"), + Color(hex: "95E1D3"), Color(hex: "F38181"), Color(hex: "AA96DA"), + Color(hex: "FCBAD3"), Color(hex: "A8D8EA"), Color(hex: "FF9671"), + Color(hex: "FFC75F"), Color(hex: "F9F871"), Color(hex: "845EC2"), + ] + var body: some View { ZStack { - // Semi-transparent backdrop - Color.black.opacity(0.1) - - // Confetti particles + // Particles — 3 layered bursts ForEach(particles) { p in - Text(p.emoji) - .font(.title) - .offset(x: p.x) - .modifier(FallingModifier(delay: p.delay, duration: p.duration, rotation: p.rotation)) + confettiShape(p) + .modifier(BurstModifier( + startX: p.x, startY: p.startY, + endY: p.endY, drift: p.drift, + delay: p.delay, duration: p.duration, spin: p.spin + )) } - // Checkmark - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 60)) - .foregroundStyle(Color.emerald) - .scaleEffect(checkScale) - .shadow(color: Color.emerald.opacity(0.3), radius: 20) + // Checkmark with glow + VStack(spacing: 8) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 56)) + .foregroundStyle(Color.emerald) + .shadow(color: Color.emerald.opacity(0.4), radius: 20) + + Text("Added!") + .font(.headline.weight(.bold)) + .foregroundStyle(Color.emerald) + } + .scaleEffect(checkScale) + .opacity(checkOpacity) } .onAppear { - // Generate particles - let emojis = ["🎉", "✨", "🍎", "🥑", "💚", "⭐️", "🎊"] - particles = (0..<15).map { i in - ConfettiParticle( - emoji: emojis[i % emojis.length], - x: CGFloat.random(in: -160...160), - delay: Double.random(in: 0...0.3), - duration: Double.random(in: 0.8...1.4), - rotation: Double.random(in: -180...180) - ) - } - // Animate checkmark - withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) { + generateParticles() + + withAnimation(.spring(response: 0.35, dampingFraction: 0.55)) { checkScale = 1.0 + checkOpacity = 1.0 } - // Haptic - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) + + // Fade out checkmark + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeOut(duration: 0.4)) { + checkOpacity = 0 + checkScale = 0.8 + } + } + + UINotificationFeedbackGenerator().notificationOccurred(.success) } } + + @ViewBuilder + private func confettiShape(_ p: ConfettiPiece) -> some View { + switch p.shape { + case 0: + Circle().fill(p.color).frame(width: p.size, height: p.size) + case 1: + RoundedRectangle(cornerRadius: 2) + .fill(p.color) + .frame(width: p.size, height: p.size * 0.5) + default: + Image(systemName: "star.fill") + .font(.system(size: p.size * 0.6)) + .foregroundStyle(p.color) + } + } + + private func generateParticles() { + var allParticles: [ConfettiPiece] = [] + + // Burst 1: Center explosion (fast, wide spread) + for _ in 0..<25 { + allParticles.append(ConfettiPiece( + color: Self.colors.randomElement()!, + size: CGFloat.random(in: 6...12), + x: CGFloat.random(in: -180...180), + startY: CGFloat.random(in: -50...50), + endY: CGFloat.random(in: 300...700), + drift: CGFloat.random(in: -80...80), + delay: Double.random(in: 0...0.1), + duration: Double.random(in: 0.8...1.2), + spin: Double.random(in: -360...360), + shape: Int.random(in: 0...2) + )) + } + + // Burst 2: Slightly delayed, wider (like canvas-confetti layering) + for _ in 0..<20 { + allParticles.append(ConfettiPiece( + color: Self.colors.randomElement()!, + size: CGFloat.random(in: 5...10), + x: CGFloat.random(in: -200...200), + startY: CGFloat.random(in: -80...0), + endY: CGFloat.random(in: 400...800), + drift: CGFloat.random(in: -120...120), + delay: Double.random(in: 0.1...0.25), + duration: Double.random(in: 1.0...1.5), + spin: Double.random(in: -540...540), + shape: Int.random(in: 0...2) + )) + } + + // Burst 3: Slow floaters (small, drifty) + for _ in 0..<15 { + allParticles.append(ConfettiPiece( + color: Self.colors.randomElement()!.opacity(0.7), + size: CGFloat.random(in: 4...8), + x: CGFloat.random(in: -160...160), + startY: CGFloat.random(in: -120...-40), + endY: CGFloat.random(in: 500...900), + drift: CGFloat.random(in: -60...60), + delay: Double.random(in: 0.2...0.4), + duration: Double.random(in: 1.3...2.0), + spin: Double.random(in: -720...720), + shape: Int.random(in: 0...1) + )) + } + + particles = allParticles + } } -struct FallingModifier: ViewModifier { +struct BurstModifier: ViewModifier { + let startX: CGFloat + let startY: CGFloat + let endY: CGFloat + let drift: CGFloat let delay: Double let duration: Double - let rotation: Double + let spin: Double - @State private var yOffset: CGFloat = -400 + @State private var progress: CGFloat = 0 @State private var opacity: Double = 1 - @State private var angle: Double = 0 func body(content: Content) -> some View { content - .offset(y: yOffset) + .offset( + x: startX + (drift * progress), + y: startY + (endY - startY) * progress + ) .opacity(opacity) - .rotationEffect(.degrees(angle)) + .rotationEffect(.degrees(spin * Double(progress))) + .scaleEffect(1.0 - Double(progress) * 0.3) .onAppear { - withAnimation(.easeIn(duration: duration).delay(delay)) { - yOffset = 600 + withAnimation(.easeOut(duration: duration).delay(delay)) { + progress = 1 + } + withAnimation(.easeIn(duration: duration * 0.4).delay(delay + duration * 0.6)) { opacity = 0 - angle = rotation } } } } -// Fix: Array doesn't have .length in Swift -extension Array { - var length: Int { count } +private extension Color { + init(hex: String) { + let scanner = Scanner(string: hex) + var rgbValue: UInt64 = 0 + scanner.scanHexInt64(&rgbValue) + self.init( + red: Double((rgbValue >> 16) & 0xFF) / 255, + green: Double((rgbValue >> 8) & 0xFF) / 255, + blue: Double(rgbValue & 0xFF) / 255 + ) + } } // MARK: - Assistant Sheet @@ -207,7 +310,7 @@ struct AssistantSheetView: View { if selectedMode == 0 { AssistantChatView(onFoodAdded: onFoodAdded) } else { - FoodSearchView(isSheet: true) + FoodSearchView(isSheet: true, onFoodAdded: onFoodAdded) } } .background(Color.canvas) diff --git a/ios/Platform/Platform/Features/Fitness/Views/AddFoodSheet.swift b/ios/Platform/Platform/Features/Fitness/Views/AddFoodSheet.swift index a53dc09..56b4256 100644 --- a/ios/Platform/Platform/Features/Fitness/Views/AddFoodSheet.swift +++ b/ios/Platform/Platform/Features/Fitness/Views/AddFoodSheet.swift @@ -3,6 +3,7 @@ import SwiftUI struct AddFoodSheet: View { @Environment(\.dismiss) private var dismiss let food: Food + var onFoodAdded: (() -> Void)? @State private var quantity: Double = 1.0 @State private var selectedMeal: MealType = .snack @@ -234,6 +235,7 @@ struct AddFoodSheet: View { ) _ = try await FitnessRepository.shared.createEntry(request) dismiss() + onFoodAdded?() } catch { self.error = error.localizedDescription } diff --git a/ios/Platform/Platform/Features/Fitness/Views/FoodSearchView.swift b/ios/Platform/Platform/Features/Fitness/Views/FoodSearchView.swift index 8a7b893..9079103 100644 --- a/ios/Platform/Platform/Features/Fitness/Views/FoodSearchView.swift +++ b/ios/Platform/Platform/Features/Fitness/Views/FoodSearchView.swift @@ -2,6 +2,7 @@ import SwiftUI struct FoodSearchView: View { var isSheet: Bool = false + var onFoodAdded: (() -> Void)? @State private var vm = FoodSearchViewModel() @State private var selectedFood: Food? @@ -48,7 +49,7 @@ struct FoodSearchView: View { vm.search() } .sheet(item: $selectedFood) { food in - AddFoodSheet(food: food) + AddFoodSheet(food: food, onFoodAdded: onFoodAdded) } }