payfrit-food-ios/PayfritFood/Views/ProductTab/ProductScreen.swift
John Pinkyfloyd 71e7ec34f6 Initial commit: PayfritFood iOS app
- SwiftUI + async/await architecture
- Barcode scanning with AVFoundation
- Product display with score ring, NOVA badge, nutrition
- Alternatives with sort/filter
- Auth (login/register)
- Favorites & history
- Account management
- Dark theme
- Connected to food.payfrit.com API (Open Food Facts proxy)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-16 16:58:21 -07:00

141 lines
4.6 KiB
Swift

import SwiftUI
struct ProductScreen: View {
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) private var dismiss
let product: Product
@State private var isFavorite = false
@State private var showAlternatives = false
@State private var isLoadingFavorite = false
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Product Image
if let imageURL = product.imageURL {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
productPlaceholder
default:
ProgressView()
}
}
.frame(height: 200)
.background(Color(.systemGray6))
.cornerRadius(16)
} else {
productPlaceholder
.frame(height: 200)
}
// Product Info
VStack(spacing: 8) {
Text(product.brand)
.font(.subheadline)
.foregroundColor(.secondary)
Text(product.name)
.font(.title2.bold())
.multilineTextAlignment(.center)
}
// Score and NOVA
HStack(spacing: 30) {
ScoreRing(score: product.score)
NOVABadge(novaGroup: product.novaGroup)
}
.padding(.vertical)
// Dietary Tags
if !product.dietaryTags.isEmpty {
DietaryPills(tags: product.dietaryTags)
}
// Nutrition Section
NutritionSection(product: product)
// Ingredients Section
IngredientsSection(ingredients: product.ingredients)
// See Alternatives Button
Button {
showAlternatives = true
} label: {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text("See Healthier Alternatives")
}
.font(.headline)
.foregroundColor(.black)
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.cornerRadius(12)
}
.padding(.top)
}
.padding()
}
.background(Color(.systemBackground))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if appState.isAuthenticated {
Button {
toggleFavorite()
} label: {
if isLoadingFavorite {
ProgressView()
} else {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.foregroundColor(isFavorite ? .red : .primary)
}
}
}
}
}
.navigationDestination(isPresented: $showAlternatives) {
AlternativesScreen(product: product)
.environmentObject(appState)
}
}
private var productPlaceholder: some View {
ZStack {
Color(.systemGray5)
Image(systemName: "carrot.fill")
.font(.system(size: 50))
.foregroundColor(.gray)
}
.cornerRadius(16)
}
private func toggleFavorite() {
isLoadingFavorite = true
Task {
do {
if isFavorite {
try await APIService.shared.removeFavorite(productId: product.id)
} else {
try await APIService.shared.addFavorite(productId: product.id)
}
await MainActor.run {
isFavorite.toggle()
isLoadingFavorite = false
}
} catch {
await MainActor.run {
isLoadingFavorite = false
appState.showError(error.localizedDescription)
}
}
}
}
}