banyaro-ios/BanYaroGo/Views/LoginView.swift
rene 68b084be97 Sechs Offline-Features: Erste Hilfe, Ausgaben, Wetter, Gassi-Zeiten, Giftköder, Verlorene
Pitch-Karte erweitert um die neuen Features (sowie Hundesitting, Züchter).

Neue DTOs in DTOs.swift:
- Expense + ExpenseCreateBody
- GassiZeit + GassiZeitCreateBody (mit wochentage [String], radius_m)
- PoisonAlert + PoisonCreateBody
- LostDog + LostDogCreateBody
- WeatherForecast + WeatherDay (mit asphalt_temp, zecken, pollen-Felder)

Neue Views:
- ErsteHilfeView + Detail: sechs Notfall-Topics (Vergiftung, Hitzschlag,
  Wunden, Atemnot, Krampfanfall, Magendrehung) — komplett offline, kein API
- AusgabenView: Liste mit Total, AddExpenseSheet mit Kategorie/Betrag/
  Datum/Hund-Picker
- WetterView: One-Shot Location + /api/weather/forecast, 7-Tage-Vorhersage
  mit Hunde-Tipps (Hitze ab 25°/30°, Frost, Asphalt ≥50°, Zecken, Regen)
- GassiZeitenView: eigene Zeiten + Add-Sheet (Wochentag-Picker, Hund-
  Auswahl), automatische lokale UNCalendarNotifications via Scheduler
- GiftkoederView: Map mit Pins + Liste in 5km Umkreis, Report-Sheet mit
  Typ-Auswahl
- VerloreneHundeView: Liste mit Foto/Distanz, Detail mit Karte

Support:
- OneShotLocation: kleiner CLLocationManager-Wrapper für einmalige
  Positionsabfrage (Wetter, Giftköder)
- GassiZeitenScheduler: UNCalendarNotificationTrigger pro Wochentag,
  Identifier-Schema "gz-{id}-{weekday}"

Navigation: Section "Hund & Alltag" im Mehr-Tab mit NavigationLinks zu
allen sechs neuen Ansichten.
2026-05-30 12:03:24 +02:00

204 lines
8.3 KiB
Swift

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())
}