payfrit-food-ios/PayfritFood/Views/AlternativesTab/AlternativesScreen.swift
John Pinkyfloyd 71e7ec34f6 Initial commit: PayfritFood iOS app
- 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>
2026-03-16 16:58:21 -07:00

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)
}
}
}
}