import SwiftUI struct LoginView: View { @Environment(AuthSession.self) private var auth @State private var email = "" @State private var password = "" @State private var pitchExpanded = false var body: some View { ScrollView { VStack(spacing: 28) { hero pitch loginCard registerCard } .padding(.horizontal, 24) .padding(.vertical, 32) } .scrollDismissesKeyboard(.interactively) } // MARK: - Hero private var hero: some View { VStack(spacing: 12) { Image("AppIconHero") .resizable() .scaledToFit() .frame(height: 120) Text("Ban Yaro Go") .font(.largeTitle.bold()) Text("Die deutschsprachige Hunde-Plattform — jetzt mit nativem GPS-Tracking.") .font(.callout) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } } // MARK: - Pitch (für Neue, ausklappbar) private var pitch: some View { VStack(spacing: 0) { Button { withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { pitchExpanded.toggle() } } label: { HStack(spacing: 12) { Image(systemName: "info.circle.fill") .foregroundStyle(Color.accentColor) VStack(alignment: .leading, spacing: 2) { Text("Was bietet banyaro.app?") .font(.subheadline.bold()) .foregroundStyle(.primary) Text(pitchExpanded ? "Tippen zum Einklappen" : "Tippen für Details") .font(.caption) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.down") .font(.callout.bold()) .foregroundStyle(.secondary) .rotationEffect(.degrees(pitchExpanded ? 180 : 0)) } .padding(18) .contentShape(Rectangle()) } .buttonStyle(.plain) if pitchExpanded { VStack(alignment: .leading, spacing: 14) { Divider() feature(icon: "map.fill", title: "Gassi-Touren aufzeichnen", subtitle: "GPS-Tracking auch im Hintergrund — mit Pause, Live Activity und HealthKit-Sync.") feature(icon: "alarm.fill", title: "Gassi-Zeiten", subtitle: "Tägliche Erinnerungen, damit keine Runde vergessen wird.") feature(icon: "person.2.fill", title: "Hunde-Community", subtitle: "Gassi-Treffen, Tierärzte und Orte in deiner Nähe.") feature(icon: "book.fill", title: "Tagebuch & Impfpass", subtitle: "Alles rund um deinen Hund an einem Ort.") feature(icon: "rosette", title: "Verifizierte Züchter", subtitle: "Züchter-Profile, aktuelle Würfe und Welpen-Vermittlung — kein Hinterhof.") feature(icon: "exclamationmark.shield.fill", title: "Giftköder-Alarm", subtitle: "Warnungen aus deiner Region direkt aufs iPhone.") feature(icon: "house.fill", title: "Hundesitting", subtitle: "Sitter und Sitter-Suche in deiner Nähe — mit Bewertungen.") feature(icon: "magnifyingglass.circle.fill", title: "Verlorene Hunde", subtitle: "Vermisstmeldungen in deinem Umkreis sehen oder selbst melden.") feature(icon: "eurosign.circle.fill", title: "Ausgaben tracken", subtitle: "Futter, Tierarzt, Versicherung — alle Hundekosten an einem Ort.") feature(icon: "cross.case.fill", title: "Erste Hilfe", subtitle: "Notfall-Anleitung für Vergiftung, Hitzschlag, Wunden — komplett offline.") feature(icon: "cloud.sun.fill", title: "Wetter für Hunde", subtitle: "Vorhersage mit Hitze- und Kältewarnung — was für deinen Hund passt.") } .padding(.horizontal, 18) .padding(.bottom, 18) } } .frame(maxWidth: .infinity, alignment: .leading) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 16)) } private func feature(icon: String, title: String, subtitle: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: icon) .font(.title3) .foregroundStyle(Color.accentColor) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text(title).font(.subheadline.bold()) Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } } } // MARK: - Login (für bestehende User) private var loginCard: some View { VStack(spacing: 12) { HStack { Text("Schon angemeldet?") .font(.headline) Spacer() } TextField("E-Mail", text: $email) .textContentType(.emailAddress) .keyboardType(.emailAddress) .textInputAutocapitalization(.never) .autocorrectionDisabled() .padding() .background(.background, in: RoundedRectangle(cornerRadius: 12)) SecureField("Passwort", text: $password) .textContentType(.password) .padding() .background(.background, in: RoundedRectangle(cornerRadius: 12)) if let error = auth.errorMessage { Text(error) .font(.footnote) .foregroundStyle(.red) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) } Button { Task { await auth.login( email: email.trimmingCharacters(in: .whitespacesAndNewlines), password: password ) } } label: { Group { if auth.isLoggingIn { ProgressView().tint(.white) } else { Text("Login").bold() } } .frame(maxWidth: .infinity, minHeight: 50) } .background(Color.accentColor, in: RoundedRectangle(cornerRadius: 12)) .foregroundStyle(.white) .disabled(auth.isLoggingIn || email.isEmpty || password.isEmpty) } .padding(18) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 16)) } // MARK: - Register (für Neue) private var registerCard: some View { VStack(spacing: 10) { Text("Neu hier?") .font(.headline) Text("banyaro.app ist **kostenlos**, **DSGVO-konform** und wird in Deutschland gehostet — kein App-Store-Konto nötig.") .font(.footnote) .foregroundStyle(.secondary) .multilineTextAlignment(.center) if let url = URL(string: "https://banyaro.app/#settings?tab=register") { Link(destination: url) { HStack(spacing: 8) { Image(systemName: "person.crop.circle.badge.plus") Text("Kostenlos registrieren").bold() Image(systemName: "arrow.up.right") .font(.caption.bold()) } .frame(maxWidth: .infinity, minHeight: 50) } .background(Color.accentColor.opacity(0.15), in: RoundedRectangle(cornerRadius: 12)) .foregroundStyle(Color.accentColor) } Text("Öffnet die Registrierung im Browser. Danach mit den neuen Zugangsdaten oben einloggen.") .font(.caption2) .foregroundStyle(.tertiary) .multilineTextAlignment(.center) } .padding(18) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 16)) } } #Preview { LoginView() .environment(AuthSession()) }