- 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>
113 lines
3.4 KiB
Swift
113 lines
3.4 KiB
Swift
import SwiftUI
|
|
|
|
struct ProductCard: View {
|
|
let product: Product
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
// Product Image
|
|
if let imageURL = product.imageURL {
|
|
AsyncImage(url: imageURL) { phase in
|
|
switch phase {
|
|
case .success(let image):
|
|
image
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
case .failure:
|
|
imagePlaceholder
|
|
default:
|
|
ProgressView()
|
|
}
|
|
}
|
|
.frame(width: 70, height: 70)
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(12)
|
|
.clipped()
|
|
} else {
|
|
imagePlaceholder
|
|
.frame(width: 70, height: 70)
|
|
}
|
|
|
|
// Product Info
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(product.brand)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(product.name)
|
|
.font(.subheadline.bold())
|
|
.lineLimit(2)
|
|
|
|
HStack(spacing: 12) {
|
|
// Score
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(scoreColor)
|
|
.frame(width: 10, height: 10)
|
|
Text("\(product.score)")
|
|
.font(.caption.bold())
|
|
}
|
|
|
|
// NOVA
|
|
Text("NOVA \(product.novaGroup)")
|
|
.font(.caption)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(novaColor.opacity(0.2))
|
|
.foregroundColor(novaColor)
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Score Ring (mini)
|
|
ZStack {
|
|
Circle()
|
|
.stroke(lineWidth: 4)
|
|
.opacity(0.2)
|
|
.foregroundColor(scoreColor)
|
|
|
|
Circle()
|
|
.trim(from: 0, to: Double(product.score) / 100.0)
|
|
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
|
.foregroundColor(scoreColor)
|
|
.rotationEffect(.degrees(-90))
|
|
|
|
Text("\(product.score)")
|
|
.font(.caption2.bold())
|
|
}
|
|
.frame(width: 40, height: 40)
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.cornerRadius(16)
|
|
}
|
|
|
|
private var imagePlaceholder: some View {
|
|
ZStack {
|
|
Color(.systemGray5)
|
|
Image(systemName: "carrot.fill")
|
|
.foregroundColor(.gray)
|
|
}
|
|
.cornerRadius(12)
|
|
}
|
|
|
|
private var scoreColor: Color {
|
|
switch product.score {
|
|
case 80...100: return .green
|
|
case 50..<80: return .yellow
|
|
case 30..<50: return .orange
|
|
default: return .red
|
|
}
|
|
}
|
|
|
|
private var novaColor: Color {
|
|
switch product.novaGroup {
|
|
case 1: return .green
|
|
case 2: return .yellow
|
|
case 3: return .orange
|
|
default: return .red
|
|
}
|
|
}
|
|
}
|