banyaro-ios/BanYaroGo/Views/RoutesListView.swift
rene 81681130e6 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)
2026-05-30 09:25:48 +02:00

111 lines
3.5 KiB
Swift

import SwiftUI
struct RoutesListView: View {
@State private var routes: [RouteListItem] = []
@State private var isLoading = false
@State private var errorMessage: String?
var body: some View {
NavigationStack {
content
.navigationTitle("Touren")
.task { await load() }
}
}
@ViewBuilder
private var content: some View {
if isLoading && routes.isEmpty {
ProgressView()
} else if let error = errorMessage, routes.isEmpty {
ContentUnavailableView(
"Konnte Touren nicht laden",
systemImage: "wifi.slash",
description: Text(error)
)
} else if routes.isEmpty {
ContentUnavailableView(
"Keine Touren",
systemImage: "map",
description: Text("Lege deine erste Gassi-Tour in der PWA an — oder warte auf Phase 2.")
)
} else {
List(routes) { route in
NavigationLink {
RouteDetailView(routeId: route.id, fallbackName: route.name)
} label: {
RouteRowView(route: route)
}
}
.refreshable { await load() }
}
}
private func load() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
routes = try await APIClient.shared.get("/api/routes")
} catch {
errorMessage = error.localizedDescription
}
}
}
struct RouteRowView: View {
let route: RouteListItem
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(route.name)
.font(.headline)
Spacer()
if let km = route.distanzKm {
Text(String(format: "%.1f km", km))
.font(.subheadline.monospacedDigit())
.foregroundStyle(.secondary)
}
}
HStack(spacing: 12) {
if let mins = route.dauerMin {
Label("\(mins) min", systemImage: "clock")
}
if let date = route.createdAt {
Text(DateUtil.format(date))
}
Spacer()
}
.font(.caption)
.foregroundStyle(.secondary)
if route.previewTrack.count >= 2 {
MiniRouteMap(track: route.previewTrack)
.frame(height: 110)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
.padding(.vertical, 4)
}
}
enum DateUtil {
/// Parses backend timestamps (SQLite `YYYY-MM-DD HH:MM:SS` or ISO-8601)
/// into a German short date.
static func format(_ input: String) -> String {
let parser = DateFormatter()
parser.locale = Locale(identifier: "en_US_POSIX")
parser.timeZone = TimeZone(identifier: "UTC")
for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ssZ"] {
parser.dateFormat = format
if let date = parser.date(from: input) {
let out = DateFormatter()
out.locale = Locale(identifier: "de_DE")
out.dateStyle = .medium
return out.string(from: date)
}
}
return String(input.prefix(10))
}
}