05/02 Phase B
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Views/Auth/LoginView.swift
|
||||
// --------------------------
|
||||
// The login screen shown when the user is not authenticated.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoginView: View {
|
||||
|
||||
@EnvironmentObject private var auth: AuthManager
|
||||
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
private enum Field { case username, password }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
// Background
|
||||
Color(.systemGroupedBackground).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
// ── Logo / Header ──────────────────────────────────────
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
Text("JanitorialQC Inspector")
|
||||
.font(.largeTitle.bold())
|
||||
|
||||
Text("Janitorial Quality Control")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.bottom, 48)
|
||||
|
||||
// ── Login Form ─────────────────────────────────────────
|
||||
VStack(spacing: 16) {
|
||||
GroupBox {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 24)
|
||||
TextField("Username", text: $username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: .username)
|
||||
.submitLabel(.next)
|
||||
.onSubmit { focusedField = .password }
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Image(systemName: "lock")
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 24)
|
||||
SecureField("Password", text: $password)
|
||||
.focused($focusedField, equals: .password)
|
||||
.submitLabel(.go)
|
||||
.onSubmit { Task { await signIn() } }
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
.frame(maxWidth: 400)
|
||||
|
||||
// Error message
|
||||
if let error = auth.errorMessage {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
// Sign In button
|
||||
Button {
|
||||
Task { await signIn() }
|
||||
} label: {
|
||||
HStack {
|
||||
if auth.isLoading {
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
Text("Sign In")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: 400)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(auth.isLoading || username.isEmpty || password.isEmpty)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// ── App Version ────────────────────────────────────────
|
||||
Text("JanitorialQC Inspector · Phase A")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func signIn() async {
|
||||
focusedField = nil
|
||||
await auth.login(username: username, password: password)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoginView()
|
||||
.environmentObject(AuthManager.shared)
|
||||
}
|
||||
Reference in New Issue
Block a user