- 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>
114 lines
3.7 KiB
Swift
114 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
struct AlternativeCard: View {
|
|
let alternative: Alternative
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
// Product Image
|
|
if let imageURL = alternative.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: 80, height: 80)
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(12)
|
|
.clipped()
|
|
} else {
|
|
imagePlaceholder
|
|
.frame(width: 80, height: 80)
|
|
}
|
|
|
|
// Product Info
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(alternative.product.brand)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(alternative.product.name)
|
|
.font(.subheadline.bold())
|
|
.lineLimit(2)
|
|
|
|
HStack(spacing: 12) {
|
|
// Score
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(scoreColor(alternative.product.score))
|
|
.frame(width: 10, height: 10)
|
|
Text("\(alternative.product.score)")
|
|
.font(.caption.bold())
|
|
}
|
|
|
|
// NOVA
|
|
Text("NOVA \(alternative.product.novaGroup)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
// Price
|
|
if let price = alternative.formattedPrice {
|
|
Text(price)
|
|
.font(.caption.bold())
|
|
.foregroundColor(.green)
|
|
}
|
|
|
|
// Distance
|
|
if let distance = alternative.formattedDistance {
|
|
Text(distance)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Score Ring (mini)
|
|
ZStack {
|
|
Circle()
|
|
.stroke(lineWidth: 4)
|
|
.opacity(0.2)
|
|
.foregroundColor(scoreColor(alternative.product.score))
|
|
|
|
Circle()
|
|
.trim(from: 0, to: Double(alternative.product.score) / 100.0)
|
|
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
|
.foregroundColor(scoreColor(alternative.product.score))
|
|
.rotationEffect(.degrees(-90))
|
|
|
|
Text("\(alternative.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 func scoreColor(_ score: Int) -> Color {
|
|
switch score {
|
|
case 80...100: return .green
|
|
case 50..<80: return .yellow
|
|
case 30..<50: return .orange
|
|
default: return .red
|
|
}
|
|
}
|
|
}
|