- Statistik-Tab raus (für Go-Companion nicht relevant)
- Mehr-Duplikate raus: Meine Hunde, Tagebuch, Wetter, Erste Hilfe sitzen
bereits auf Heim als Quick-Action bzw. im Dog-Picker
- Im PWA ist 'Gassi' der social walks-Bereich (walks.py) und 'Stamm-Gassi-
Zeiten' nur ein Tab darin (Community-Pool, gassi_zeiten.py). Meine
Implementierung als 'tägliche Erinnerungen' war fachlich falsch:
+ Mehr-Eintrag heißt jetzt 'Stamm-Gassi-Zeiten'
+ ContentUnavailableView + Footer erklären die Community-Komponente
+ Pitch-Karte unterscheidet jetzt klar: 'Gassi-Treffen' (sich verabreden)
und 'Stamm-Gassi-Zeiten' (regelmäßige Runden + Pool)
+ 'Hunde-Orte' getrennt als eigener Pitch-Punkt
205 lines
8.5 KiB
Swift
205 lines
8.5 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: "person.2.fill", title: "Gassi-Treffen", subtitle: "Triff andere Hundebesitzer in deiner Nähe — verabrede dich für gemeinsame Runden.")
|
|
feature(icon: "alarm.fill", title: "Stamm-Gassi-Zeiten", subtitle: "Trag ein, wann du regelmäßig läufst — bekommst Erinnerungen und triffst Gleichgesinnte.")
|
|
feature(icon: "mappin.and.ellipse", title: "Hunde-Orte", subtitle: "Tierärzte, Hundeparks und gute Plätze 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())
|
|
}
|