- 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>
145 lines
5.4 KiB
Swift
145 lines
5.4 KiB
Swift
import SwiftUI
|
|
|
|
struct AlternativesScreen: View {
|
|
@EnvironmentObject var appState: AppState
|
|
let product: Product
|
|
|
|
@State private var alternatives: [Alternative] = []
|
|
@State private var isLoading = true
|
|
@State private var selectedSort: SortOption = .rating
|
|
@State private var activeFilters: Set<FilterOption> = []
|
|
|
|
enum SortOption: String, CaseIterable {
|
|
case rating = "Rating"
|
|
case price = "Price"
|
|
case distance = "Distance"
|
|
case processing = "Processing Level"
|
|
}
|
|
|
|
enum FilterOption: String, CaseIterable {
|
|
case vegan = "Vegan"
|
|
case vegetarian = "Vegetarian"
|
|
case glutenFree = "Gluten-Free"
|
|
case dairyFree = "Dairy-Free"
|
|
case nutFree = "Nut-Free"
|
|
case organic = "Organic"
|
|
case delivery = "Delivery"
|
|
case pickup = "Pickup"
|
|
case buyOnline = "Buy Online"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Sort and Filter Controls
|
|
VStack(spacing: 12) {
|
|
// Sort Picker
|
|
HStack {
|
|
Text("Sort by:")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
Picker("Sort", selection: $selectedSort) {
|
|
ForEach(SortOption.allCases, id: \.self) { option in
|
|
Text(option.rawValue).tag(option)
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
// Filter Chips
|
|
FilterChips(activeFilters: $activeFilters)
|
|
}
|
|
.padding(.vertical, 12)
|
|
.background(Color(.secondarySystemBackground))
|
|
|
|
// Alternatives List
|
|
if isLoading {
|
|
Spacer()
|
|
ProgressView("Finding alternatives...")
|
|
Spacer()
|
|
} else if filteredAlternatives.isEmpty {
|
|
Spacer()
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "magnifyingglass")
|
|
.font(.system(size: 50))
|
|
.foregroundColor(.secondary)
|
|
Text("No alternatives found")
|
|
.font(.headline)
|
|
Text("Try adjusting your filters")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
Spacer()
|
|
} else {
|
|
ScrollView {
|
|
LazyVStack(spacing: 12) {
|
|
ForEach(filteredAlternatives) { alternative in
|
|
if alternative.isSponsored && !appState.isPremium {
|
|
SponsoredCard(alternative: alternative)
|
|
} else if !alternative.isSponsored {
|
|
AlternativeCard(alternative: alternative)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Alternatives")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task {
|
|
await loadAlternatives()
|
|
}
|
|
.onChange(of: selectedSort) { _ in
|
|
Task { await loadAlternatives() }
|
|
}
|
|
}
|
|
|
|
private var filteredAlternatives: [Alternative] {
|
|
alternatives.filter { alternative in
|
|
if activeFilters.isEmpty { return true }
|
|
|
|
for filter in activeFilters {
|
|
switch filter {
|
|
case .vegan:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased() == "vegan" }) { return false }
|
|
case .vegetarian:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased() == "vegetarian" }) { return false }
|
|
case .glutenFree:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased().contains("gluten") }) { return false }
|
|
case .dairyFree:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased().contains("dairy") }) { return false }
|
|
case .nutFree:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased().contains("nut") }) { return false }
|
|
case .organic:
|
|
if !alternative.product.dietaryTags.contains(where: { $0.lowercased() == "organic" }) { return false }
|
|
case .delivery:
|
|
if !alternative.hasDelivery { return false }
|
|
case .pickup:
|
|
if !alternative.hasPickup { return false }
|
|
case .buyOnline:
|
|
if !alternative.hasBuyLink { return false }
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func loadAlternatives() async {
|
|
isLoading = true
|
|
do {
|
|
let sortParam = selectedSort.rawValue.lowercased().replacingOccurrences(of: " ", with: "_")
|
|
let result = try await APIService.shared.getAlternatives(productId: product.id, sort: sortParam)
|
|
await MainActor.run {
|
|
alternatives = result
|
|
isLoading = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isLoading = false
|
|
appState.showError(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|