Ban Yaro Go — Phase 1 Foundation
SwiftUI/SwiftData iOS-Client, redet mit https://banyaro.app FastAPI-Backend. Bundle-ID app.banyaro.ios, Xcode-26-Projekt mit synchronisierten Ordnern. Drin: - APIClient (URLSession + Bearer + convertFromSnakeCase Decoder) - KeychainStore + AuthSession (@Observable) für persistenten Login - LoginView, MainTabView, SettingsView (mit Logout) - RoutesListView + RouteDetailView mit MapKit-Polyline aus preview_track - DogsListView mit Foto-Avatar - App-Icon (Pfote auf Banyaro-Amber)
This commit is contained in:
commit
81681130e6
20 changed files with 1129 additions and 0 deletions
96
BanYaroGo/Views/DogsListView.swift
Normal file
96
BanYaroGo/Views/DogsListView.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import SwiftUI
|
||||
|
||||
struct DogsListView: View {
|
||||
@State private var dogs: [Dog] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
content
|
||||
.navigationTitle("Hunde")
|
||||
.task { await load() }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if isLoading && dogs.isEmpty {
|
||||
ProgressView()
|
||||
} else if let error = errorMessage, dogs.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"Konnte Hunde nicht laden",
|
||||
systemImage: "wifi.slash",
|
||||
description: Text(error)
|
||||
)
|
||||
} else if dogs.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"Keine Hunde",
|
||||
systemImage: "pawprint",
|
||||
description: Text("Lege deinen ersten Hund in der PWA an.")
|
||||
)
|
||||
} else {
|
||||
List(dogs) { dog in
|
||||
DogRow(dog: dog)
|
||||
}
|
||||
.refreshable { await load() }
|
||||
}
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
do {
|
||||
dogs = try await APIClient.shared.get("/api/dogs")
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DogRow: View {
|
||||
let dog: Dog
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
avatar
|
||||
.frame(width: 56, height: 56)
|
||||
.background(.background.secondary)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(dog.name).font(.headline)
|
||||
if let rasse = dog.rasse, !rasse.isEmpty {
|
||||
Text(rasse)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var avatar: some View {
|
||||
if let path = dog.fotoUrl, let url = URL(string: "https://banyaro.app\(path)") {
|
||||
AsyncImage(url: url) { phase in
|
||||
switch phase {
|
||||
case .success(let img):
|
||||
img.resizable().scaledToFill()
|
||||
default:
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
Image(systemName: "pawprint.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue