import SwiftUI import MapKit struct VerloreneHundeView: View { @State private var location = OneShotLocation() @State private var lostDogs: [LostDog] = [] @State private var isLoading = false @State private var errorMessage: String? var body: some View { content .navigationTitle("Verlorene Hunde") .navigationBarTitleDisplayMode(.inline) .task { location.request() await load() } .onChange(of: location.coordinate?.latitude) { _, _ in Task { await load() } } .refreshable { await load() } } @ViewBuilder private var content: some View { if isLoading && lostDogs.isEmpty { ProgressView() } else if let errorMessage, lostDogs.isEmpty { ContentUnavailableView("Konnte nicht laden", systemImage: "wifi.slash", description: Text(errorMessage)) } else if lostDogs.isEmpty { ContentUnavailableView( "Keine vermissten Hunde", systemImage: "checkmark.circle", description: Text("In 25 km Umkreis sind aktuell keine Vermisstmeldungen aktiv.") ) } else { List(lostDogs) { dog in NavigationLink { LostDogDetailView(dog: dog) } label: { LostDogRow(dog: dog) } } } } private func load() async { isLoading = true errorMessage = nil defer { isLoading = false } do { var path = "/api/lost?radius_km=25" if let coord = location.coordinate { path = "/api/lost?lat=\(coord.latitude)&lon=\(coord.longitude)&radius_km=25" } lostDogs = try await APIClient.shared.get(path) } catch { errorMessage = error.localizedDescription } } } private struct LostDogRow: View { let dog: LostDog var body: some View { HStack(spacing: 12) { avatar .frame(width: 56, height: 56) .clipShape(RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 2) { HStack { Text(dog.name).font(.headline) Spacer() if let d = dog.distanzM { Text(distLabel(d)) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } if let r = dog.rasse, !r.isEmpty { Text(r).font(.caption).foregroundStyle(.secondary) } Text(dog.beschreibung) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) } } .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 { ZStack { Color.accentColor.opacity(0.15) Image(systemName: "magnifyingglass") .foregroundStyle(Color.accentColor) } } private func distLabel(_ m: Int) -> String { if m >= 1000 { return String(format: "%.1f km", Double(m) / 1000) } return "\(m) m" } } private struct LostDogDetailView: View { let dog: LostDog var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { 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().scaledToFit() default: Rectangle().fill(.gray.opacity(0.15)).frame(height: 200) } } .frame(maxHeight: 280) .clipShape(RoundedRectangle(cornerRadius: 12)) } VStack(alignment: .leading, spacing: 6) { Text(dog.name).font(.title.bold()) if let r = dog.rasse, !r.isEmpty { Text(r).font(.headline).foregroundStyle(.secondary) } } Text(dog.beschreibung).font(.body) Map(initialPosition: .region(MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: dog.lat, longitude: dog.lon), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ))) { Annotation(dog.name, coordinate: CLLocationCoordinate2D(latitude: dog.lat, longitude: dog.lon)) { Image(systemName: "magnifyingglass.circle.fill") .font(.title) .foregroundStyle(.white, Color.accentColor) .background(.white, in: Circle()) } } .frame(height: 240) .clipShape(RoundedRectangle(cornerRadius: 12)) .allowsHitTesting(false) if let m = dog.melderName { Label("Gemeldet von \(m)", systemImage: "person.fill") .font(.caption).foregroundStyle(.secondary) } } .padding() } .navigationTitle("Vermisst") .navigationBarTitleDisplayMode(.inline) } }