- 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>
142 lines
4.6 KiB
Swift
142 lines
4.6 KiB
Swift
import SwiftUI
|
|
|
|
struct SponsoredCard: View {
|
|
let alternative: Alternative
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
// Sponsored label
|
|
HStack {
|
|
Text("Sponsored")
|
|
.font(.caption2.bold())
|
|
.foregroundColor(.secondary)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color(.systemGray5))
|
|
.cornerRadius(4)
|
|
Spacer()
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// Price
|
|
if let price = alternative.formattedPrice {
|
|
Text(price)
|
|
.font(.caption.bold())
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
// Action Buttons
|
|
HStack(spacing: 8) {
|
|
if let deliveryUrl = alternative.deliveryUrl, let url = URL(string: deliveryUrl) {
|
|
Link(destination: url) {
|
|
ActionButton(icon: "truck.box.fill", label: "Delivery")
|
|
}
|
|
}
|
|
|
|
if let pickupUrl = alternative.pickupUrl, let url = URL(string: pickupUrl) {
|
|
Link(destination: url) {
|
|
ActionButton(icon: "building.2.fill", label: "Pickup")
|
|
}
|
|
}
|
|
|
|
if let buyUrl = alternative.buyUrl, let url = URL(string: buyUrl) {
|
|
Link(destination: url) {
|
|
ActionButton(icon: "cart.fill", label: "Buy")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.stroke(Color.green.opacity(0.3), lineWidth: 1)
|
|
)
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActionButton: View {
|
|
let icon: String
|
|
let label: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: icon)
|
|
.font(.caption)
|
|
Text(label)
|
|
.font(.caption.bold())
|
|
}
|
|
.foregroundColor(.green)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(Color.green.opacity(0.15))
|
|
.cornerRadius(8)
|
|
}
|
|
}
|