feat: calorie tap → fitness, confetti on food add, reduce dashboard padding
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s

- Calorie widget taps switches to Fitness tab
- After adding food: dismiss sheet, switch to Fitness, confetti animation
- Confetti: emoji particles falling + checkmark + haptic feedback
- Dashboard top padding reduced from 60pt to 16pt
- HomeView accepts selectedTab binding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 09:34:48 -05:00
parent 04fbdc9a9b
commit b01409628f
3 changed files with 146 additions and 25 deletions

View File

@@ -24,11 +24,12 @@ struct ContentView: View {
struct MainTabView: View { struct MainTabView: View {
@State private var selectedTab = 0 @State private var selectedTab = 0
@State private var showAssistant = false @State private var showAssistant = false
@State private var showSuccess = false
var body: some View { var body: some View {
ZStack(alignment: .bottomTrailing) { ZStack {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
HomeView() HomeView(selectedTab: $selectedTab)
.tabItem { .tabItem {
Label("Home", systemImage: "house.fill") Label("Home", systemImage: "house.fill")
} }
@@ -42,35 +43,146 @@ struct MainTabView: View {
} }
.tint(Color.accentWarm) .tint(Color.accentWarm)
Button { // Floating + button
showAssistant = true VStack {
} label: { Spacer()
Image(systemName: "plus") HStack {
.font(.title2.weight(.semibold)) Spacer()
.foregroundStyle(.white) Button {
.frame(width: 56, height: 56) showAssistant = true
.background(Color.accentWarm) } label: {
.clipShape(Circle()) Image(systemName: "plus")
.shadow(color: .black.opacity(0.2), radius: 8, y: 4) .font(.title2.weight(.semibold))
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(Color.accentWarm)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 8, y: 4)
}
.padding(.trailing, 20)
}
.padding(.bottom, 70)
}
// Success overlay
if showSuccess {
SuccessConfettiView()
.ignoresSafeArea()
.allowsHitTesting(false)
} }
.padding(.trailing, 20)
.padding(.bottom, 70)
} }
.sheet(isPresented: $showAssistant) { .sheet(isPresented: $showAssistant) {
AssistantSheetView() AssistantSheetView(onFoodAdded: {
showAssistant = false
selectedTab = 1
// Show confetti
withAnimation { showSuccess = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
withAnimation { showSuccess = false }
}
})
} }
} }
} }
// MARK: - Success Confetti Animation
struct SuccessConfettiView: View {
@State private var particles: [ConfettiParticle] = []
@State private var checkScale: CGFloat = 0
struct ConfettiParticle: Identifiable {
let id = UUID()
let emoji: String
let x: CGFloat
let delay: Double
let duration: Double
let rotation: Double
}
var body: some View {
ZStack {
// Semi-transparent backdrop
Color.black.opacity(0.1)
// Confetti particles
ForEach(particles) { p in
Text(p.emoji)
.font(.title)
.offset(x: p.x)
.modifier(FallingModifier(delay: p.delay, duration: p.duration, rotation: p.rotation))
}
// Checkmark
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.foregroundStyle(Color.emerald)
.scaleEffect(checkScale)
.shadow(color: Color.emerald.opacity(0.3), radius: 20)
}
.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)) {
checkScale = 1.0
}
// Haptic
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
}
}
struct FallingModifier: ViewModifier {
let delay: Double
let duration: Double
let rotation: Double
@State private var yOffset: CGFloat = -400
@State private var opacity: Double = 1
@State private var angle: Double = 0
func body(content: Content) -> some View {
content
.offset(y: yOffset)
.opacity(opacity)
.rotationEffect(.degrees(angle))
.onAppear {
withAnimation(.easeIn(duration: duration).delay(delay)) {
yOffset = 600
opacity = 0
angle = rotation
}
}
}
}
// Fix: Array doesn't have .length in Swift
extension Array {
var length: Int { count }
}
// MARK: - Assistant Sheet
struct AssistantSheetView: View { struct AssistantSheetView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var selectedMode = 0 @State private var selectedMode = 0
var onFoodAdded: () -> Void = {}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Custom header warm background // Custom header
VStack(spacing: 12) { VStack(spacing: 12) {
// Drag handle
Capsule() Capsule()
.fill(Color.textTertiary.opacity(0.3)) .fill(Color.textTertiary.opacity(0.3))
.frame(width: 36, height: 5) .frame(width: 36, height: 5)
@@ -80,7 +192,6 @@ struct AssistantSheetView: View {
.font(.headline) .font(.headline)
.foregroundStyle(Color.textPrimary) .foregroundStyle(Color.textPrimary)
// Tab picker warm styled
HStack(spacing: 4) { HStack(spacing: 4) {
tabButton("AI Chat", icon: "sparkles", index: 0) tabButton("AI Chat", icon: "sparkles", index: 0)
tabButton("Quick Add", icon: "magnifyingglass", index: 1) tabButton("Quick Add", icon: "magnifyingglass", index: 1)
@@ -93,9 +204,8 @@ struct AssistantSheetView: View {
.padding(.bottom, 12) .padding(.bottom, 12)
.background(Color.canvas) .background(Color.canvas)
// Content
if selectedMode == 0 { if selectedMode == 0 {
AssistantChatView() AssistantChatView(onFoodAdded: onFoodAdded)
} else { } else {
FoodSearchView(isSheet: true) FoodSearchView(isSheet: true)
} }

View File

@@ -3,6 +3,7 @@ import PhotosUI
struct AssistantChatView: View { struct AssistantChatView: View {
@State private var vm = AssistantViewModel() @State private var vm = AssistantViewModel()
var onFoodAdded: (() -> Void)?
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -113,6 +114,13 @@ struct AssistantChatView: View {
.onChange(of: vm.selectedPhoto) { .onChange(of: vm.selectedPhoto) {
Task { await vm.handlePhotoSelection() } Task { await vm.handlePhotoSelection() }
} }
.onChange(of: vm.applied) {
if vm.applied {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
onFoodAdded?()
}
}
}
} }
// MARK: - Chat Bubble // MARK: - Chat Bubble

View File

@@ -4,8 +4,8 @@ import PhotosUI
struct HomeView: View { struct HomeView: View {
@Environment(AuthManager.self) private var auth @Environment(AuthManager.self) private var auth
@State private var vm = HomeViewModel() @State private var vm = HomeViewModel()
@State private var showAssistant = false
@State private var ringAnimated = false @State private var ringAnimated = false
@Binding var selectedTab: Int
var body: some View { var body: some View {
ZStack { ZStack {
@@ -54,12 +54,15 @@ struct HomeView: View {
} }
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 60) .padding(.top, 16)
// Widget grid half width // Widget grid half width, tap to go to fitness
HStack(spacing: 12) { HStack(spacing: 12) {
calorieWidget Button { selectedTab = 1 } label: {
// Future widget placeholder invisible spacer calorieWidget
}
.buttonStyle(.plain)
Color.clear Color.clear
} }
.padding(.horizontal) .padding(.horizontal)