- 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>
141 lines
4.6 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|