- 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>
118 lines
3.7 KiB
Swift
118 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
struct LoginSheet: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var email = ""
|
|
@State private var password = ""
|
|
@State private var isLoading = false
|
|
@State private var error: String?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 24) {
|
|
// Logo
|
|
Image(systemName: "leaf.circle.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.green)
|
|
.padding(.top, 40)
|
|
|
|
Text("Welcome Back")
|
|
.font(.title.bold())
|
|
|
|
Text("Log in to save favorites and view your scan history.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
|
|
// Form
|
|
VStack(spacing: 16) {
|
|
TextField("Email", text: $email)
|
|
.keyboardType(.emailAddress)
|
|
.textContentType(.emailAddress)
|
|
.autocapitalization(.none)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
SecureField("Password", text: $password)
|
|
.textContentType(.password)
|
|
.textFieldStyle(.roundedBorder)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
if let error = error {
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
|
|
// Login Button
|
|
Button {
|
|
login()
|
|
} label: {
|
|
if isLoading {
|
|
ProgressView()
|
|
.tint(.black)
|
|
} else {
|
|
Text("Log In")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
.foregroundColor(.black)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(isValid ? Color.green : Color.gray)
|
|
.cornerRadius(12)
|
|
.disabled(!isValid || isLoading)
|
|
.padding(.horizontal, 24)
|
|
|
|
// Register Link
|
|
HStack {
|
|
Text("Don't have an account?")
|
|
.foregroundColor(.secondary)
|
|
Button("Sign Up") {
|
|
dismiss()
|
|
appState.showRegisterSheet = true
|
|
}
|
|
.foregroundColor(.green)
|
|
}
|
|
.font(.subheadline)
|
|
|
|
Spacer()
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Cancel") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isValid: Bool {
|
|
!email.isEmpty && email.contains("@") && !password.isEmpty
|
|
}
|
|
|
|
private func login() {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
Task {
|
|
do {
|
|
let (profile, token) = try await APIService.shared.login(email: email, password: password)
|
|
await appState.setAuth(userId: profile.id, token: token, profile: profile)
|
|
await MainActor.run {
|
|
dismiss()
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error.localizedDescription
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|