Aggiunge app iOS nativa con parità multi-sport e test unitari.

Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-14 21:37:32 +02:00
co-authored by Cursor
parent b798e0ea06
commit 585332e32e
101 changed files with 7910 additions and 5 deletions
@@ -0,0 +1,125 @@
import SwiftUI
struct LoginScreen: View {
@ObservedObject var container: AppContainer
let onLoggedIn: () -> Void
@State private var email = ""
@State private var password = ""
@State private var error: String?
@State private var loading = false
@State private var passwordVisible = false
var body: some View {
MatchScreenScaffold {
ScrollView {
VStack(spacing: 0) {
MatchLiveWordmark(showSlogan: true)
.padding(.top, 32)
Text("ACCEDI")
.font(MatchTypography.headlineMedium)
.padding(.top, 48)
Text("Gestisci le dirette della tua squadra")
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.multilineTextAlignment(.center)
.padding(.top, 8)
VStack(spacing: 16) {
MatchTextField(title: "Email", text: $email, placeholder: "coach@squadra.it", keyboard: .emailAddress)
MatchSecureField(title: "Password", text: $password, visible: $passwordVisible)
}
.padding(.top, 32)
if let error {
Text(error)
.foregroundStyle(MatchColors.primaryRed)
.multilineTextAlignment(.center)
.padding(.top, 12)
}
MatchPrimaryButton(
label: "ACCEDI",
action: submitLogin,
enabled: !email.isEmpty && !password.isEmpty,
loading: loading
)
.padding(.top, 32)
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
}
}
private func submitLogin() {
guard !loading, !email.isEmpty, !password.isEmpty else { return }
loading = true
error = nil
Task {
do {
_ = try await container.authRepository.login(email: email.trimmingCharacters(in: .whitespaces), password: password)
onLoggedIn()
} catch {
let message = error.localizedDescription
if message.contains("401") {
self.error = "Email o password non corretti"
} else if message.localizedCaseInsensitiveContains("timeout") {
self.error = "Server non raggiungibile. Verifica la connessione."
} else {
self.error = message
}
}
loading = false
}
}
}
private struct MatchTextField: View {
let title: String
@Binding var text: String
var placeholder: String = ""
var keyboard: UIKeyboardType = .default
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
TextField(placeholder, text: $text)
.textInputAutocapitalization(.never)
.keyboardType(keyboard)
.autocorrectionDisabled()
.padding(12)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
}
}
}
private struct MatchSecureField: View {
let title: String
@Binding var text: String
@Binding var visible: Bool
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
HStack {
Group {
if visible {
TextField("", text: $text)
} else {
SecureField("", text: $text)
}
}
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button(action: { visible.toggle() }) {
Image(systemName: visible ? "eye.slash" : "eye")
.foregroundStyle(MatchColors.textSecondary)
}
}
.padding(12)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
}
}
}