159 lines
6.1 KiB
Swift
159 lines
6.1 KiB
Swift
import SwiftUI
|
|
|
|
struct LoginView: View {
|
|
@Environment(AuthManager.self) private var authManager
|
|
|
|
@State private var username = ""
|
|
@State private var password = ""
|
|
@State private var isLoading = false
|
|
@FocusState private var focusedField: Field?
|
|
|
|
private enum Field: Hashable {
|
|
case username, password
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.canvas
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 32) {
|
|
Spacer()
|
|
.frame(height: 60)
|
|
|
|
// Logo / Branding
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "square.grid.2x2.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(Color.accentWarm)
|
|
|
|
Text("Platform")
|
|
.font(.largeTitle.weight(.bold))
|
|
.foregroundStyle(Color.text1)
|
|
|
|
Text("Sign in to your dashboard")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.text3)
|
|
}
|
|
|
|
// Form
|
|
VStack(spacing: 16) {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Username")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(Color.text3)
|
|
.textCase(.uppercase)
|
|
|
|
TextField("Enter username", text: $username)
|
|
.textFieldStyle(.plain)
|
|
.textContentType(.username)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
.focused($focusedField, equals: .username)
|
|
.padding(14)
|
|
.background(Color.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
focusedField == .username ? Color.accentWarm : Color.black.opacity(0.06),
|
|
lineWidth: focusedField == .username ? 2 : 1
|
|
)
|
|
)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Password")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(Color.text3)
|
|
.textCase(.uppercase)
|
|
|
|
SecureField("Enter password", text: $password)
|
|
.textFieldStyle(.plain)
|
|
.textContentType(.password)
|
|
.focused($focusedField, equals: .password)
|
|
.padding(14)
|
|
.background(Color.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
focusedField == .password ? Color.accentWarm : Color.black.opacity(0.06),
|
|
lineWidth: focusedField == .password ? 2 : 1
|
|
)
|
|
)
|
|
}
|
|
|
|
if let error = authManager.loginError {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "exclamationmark.circle.fill")
|
|
.foregroundStyle(Color.error)
|
|
Text(error)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.error)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.top, 4)
|
|
}
|
|
}
|
|
.padding(.horizontal, 4)
|
|
|
|
// Sign In Button
|
|
Button {
|
|
performLogin()
|
|
} label: {
|
|
HStack(spacing: 8) {
|
|
if isLoading {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
.tint(.white)
|
|
}
|
|
Text("Sign In")
|
|
.font(.body.weight(.semibold))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 16)
|
|
.background(canSubmit ? Color.accentWarm : Color.text4)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
.disabled(!canSubmit)
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 28)
|
|
}
|
|
}
|
|
.onSubmit {
|
|
switch focusedField {
|
|
case .username:
|
|
focusedField = .password
|
|
case .password:
|
|
performLogin()
|
|
case .none:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private var canSubmit: Bool {
|
|
!username.trimmingCharacters(in: .whitespaces).isEmpty
|
|
&& !password.isEmpty
|
|
&& !isLoading
|
|
}
|
|
|
|
private func performLogin() {
|
|
guard canSubmit else { return }
|
|
isLoading = true
|
|
focusedField = nil
|
|
Task {
|
|
await authManager.login(
|
|
username: username.trimmingCharacters(in: .whitespaces),
|
|
password: password
|
|
)
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|