medmap-ios/medmap/Views/LoginView.swift
2026-09-15 23:23:01 +01:00

200 lines
6.1 KiB
Swift

//
// LoginView.swift
// medmap
//
import SwiftUI
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
@FocusState private var focusedField: Field?
private enum Field {
case email
case password
}
var body: some View {
ZStack {
Color.appBackground.ignoresSafeArea()
formPanel
}
//.preferredColorScheme(.light)
}
private var formPanel: some View {
VStack(spacing: 0) {
HStack {
Image("splashLogo")
.resizable()
.scaledToFit()
.frame(width: 150, height: 180)
.accessibilityHidden(true)
Spacer()
}
.padding(.top, 32)
.padding(.horizontal, 32)
Spacer()
VStack(alignment: .leading) {
Text("Aceda aqui à sua conta")
.font(.jakartaBold24)
.foregroundStyle(.white)
VStack(spacing: 14) {
loginField(
placeholder: "Email",
icon: "envelope",
text: $viewModel.email,
field: .email,
contentType: .emailAddress,
keyboardType: .emailAddress
)
passwordField
}
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.font(.jakartaRegular14)
.foregroundStyle(Color.loginErrorText)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.accessibilityLabel("Erro de autenticacao: \(errorMessage)")
}
Button {
focusedField = nil
Task {
await viewModel.login()
}
} label: {
Group {
if viewModel.isLoading {
ProgressView()
.tint(Color.loginButtonText)
} else {
Text("Entrar")
.font(.jakartaBold16)
}
}
.frame(maxWidth: .infinity)
.frame(height: 46)
}
.foregroundStyle(Color.loginButtonText)
.background(Color.loginButtonBackground)
.clipShape(RoundedRectangle(cornerRadius: 8))
.disabled(!viewModel.canSubmit)
.opacity(viewModel.canSubmit ? 1 : 0.55)
.accessibilityHint("Inicia sessao na conta de doente")
}
.padding(.horizontal, 32)
Spacer()
Spacer()
VStack(spacing: 8) {
Button("Não tem conta? Criar conta") {
viewModel.showSignup()
}
.font(.jakartaRegular14)
.foregroundStyle(.white.opacity(0.88))
Text("MedMap")
.font(.jakartaRegular12)
.foregroundStyle(.white.opacity(0.6))
Image("corporateLogos")
}
.padding(.bottom, 12)
}
}
private var passwordField: some View {
loginFieldContainer(icon: "lock") {
ZStack(alignment: .leading) {
if viewModel.password.isEmpty {
Text("Palavra-passe")
.foregroundStyle(Color.loginPlaceholder)
}
SecureField("", text: $viewModel.password)
.foregroundStyle(.white)
.tint(.white)
.textContentType(.password)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
Task {
await viewModel.login()
}
}
}
}
.accessibilityLabel("Palavra-passe")
}
private func loginField(
placeholder: String,
icon: String,
text: Binding<String>,
field: Field,
contentType: UITextContentType,
keyboardType: UIKeyboardType
) -> some View {
loginFieldContainer(icon: icon) {
ZStack(alignment: .leading) {
if text.wrappedValue.isEmpty {
Text(placeholder)
.foregroundStyle(Color.loginPlaceholder)
}
TextField("", text: text)
.foregroundStyle(.white)
.tint(.white)
.textContentType(contentType)
.keyboardType(keyboardType)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: field)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
}
}
.accessibilityLabel(placeholder)
}
private func loginFieldContainer<Content: View>(
icon: String,
@ViewBuilder content: () -> Content
) -> some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.white)
.frame(width: 18)
content()
.font(.jakartaRegular16)
}
.padding(.horizontal, 14)
.frame(height: 46)
.background(Color.loginFieldBackground)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.loginFieldBorder, lineWidth: 1)
}
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
#Preview {
LoginView()
}