- 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>
191 lines
6.2 KiB
Swift
191 lines
6.2 KiB
Swift
import SwiftUI
|
|
|
|
struct HistoryScreen: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var history: [ScanHistoryItem] = []
|
|
@State private var isLoading = true
|
|
@State private var selectedProduct: Product?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if !appState.isAuthenticated {
|
|
// Not logged in
|
|
VStack(spacing: 20) {
|
|
Spacer()
|
|
Image(systemName: "clock.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.gray)
|
|
|
|
Text("Track Your Scans")
|
|
.font(.title2.bold())
|
|
|
|
Text("Log in to keep a history of all the products you've scanned.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
Button {
|
|
appState.showLoginSheet = true
|
|
} label: {
|
|
Text("Log In")
|
|
.font(.headline)
|
|
.foregroundColor(.black)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.green)
|
|
.cornerRadius(12)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
|
|
Spacer()
|
|
}
|
|
} else if isLoading {
|
|
ProgressView("Loading history...")
|
|
} else if history.isEmpty {
|
|
VStack(spacing: 20) {
|
|
Spacer()
|
|
Image(systemName: "clock")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.gray)
|
|
|
|
Text("No Scan History")
|
|
.font(.title2.bold())
|
|
|
|
Text("Products you scan will appear here.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
Spacer()
|
|
}
|
|
} else {
|
|
List {
|
|
ForEach(history) { item in
|
|
HistoryRow(item: item)
|
|
.onTapGesture {
|
|
selectedProduct = item.product
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
}
|
|
.navigationTitle("History")
|
|
.navigationDestination(isPresented: Binding(
|
|
get: { selectedProduct != nil },
|
|
set: { if !$0 { selectedProduct = nil } }
|
|
)) {
|
|
if let product = selectedProduct {
|
|
ProductScreen(product: product)
|
|
.environmentObject(appState)
|
|
}
|
|
}
|
|
.task {
|
|
if appState.isAuthenticated {
|
|
await loadHistory()
|
|
}
|
|
}
|
|
.refreshable {
|
|
await loadHistory()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadHistory() async {
|
|
isLoading = true
|
|
do {
|
|
let result = try await APIService.shared.getHistory()
|
|
await MainActor.run {
|
|
history = result
|
|
isLoading = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isLoading = false
|
|
appState.showError(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HistoryRow: View {
|
|
let item: ScanHistoryItem
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
// Product Image
|
|
if let imageURL = item.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: 50, height: 50)
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(8)
|
|
.clipped()
|
|
} else {
|
|
imagePlaceholder
|
|
.frame(width: 50, height: 50)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(item.product.name)
|
|
.font(.subheadline.bold())
|
|
.lineLimit(1)
|
|
|
|
Text(item.product.brand)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 4) {
|
|
// Score
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(scoreColor(item.product.score))
|
|
.frame(width: 8, height: 8)
|
|
Text("\(item.product.score)")
|
|
.font(.caption.bold())
|
|
}
|
|
|
|
// Time ago
|
|
Text(item.formattedDate)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
private var imagePlaceholder: some View {
|
|
ZStack {
|
|
Color(.systemGray5)
|
|
Image(systemName: "carrot.fill")
|
|
.font(.caption)
|
|
.foregroundColor(.gray)
|
|
}
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|