import SwiftUI import CoreLocation struct WetterView: View { @State private var location = OneShotLocation() @State private var forecast: WeatherForecast? @State private var isLoading = false @State private var errorMessage: String? var body: some View { Group { if let forecast { List { Section { ForEach(Array(forecast.days.prefix(7))) { day in WeatherDayRow(day: day) } } footer: { Text("Vorhersage von Open-Meteo. Hunde-Tipps basieren auf maximaler Tagestemperatur und Asphalt-Hitze.") .font(.caption2) .foregroundStyle(.tertiary) } } } else if isLoading { ProgressView("Lade Wetter…") } else if let errorMessage { ContentUnavailableView( "Wetter konnte nicht geladen werden", systemImage: "cloud.slash", description: Text(errorMessage) ) } else if location.error != nil { ContentUnavailableView( "Kein Standort", systemImage: "location.slash", description: Text(location.error ?? "Bitte Standort erlauben.") ) } else { ProgressView("Hole Standort…") } } .navigationTitle("Wetter") .navigationBarTitleDisplayMode(.inline) .task { location.request() } .onChange(of: location.coordinate?.latitude) { _, _ in Task { await load() } } .refreshable { await load() } } private func load() async { guard let coord = location.coordinate else { return } isLoading = true errorMessage = nil defer { isLoading = false } do { forecast = try await APIClient.shared.get( "/api/weather/forecast?lat=\(coord.latitude)&lon=\(coord.longitude)" ) } catch { errorMessage = error.localizedDescription } } } private struct WeatherDayRow: View { let day: WeatherDay private var dateLabel: String { if day.date == today { return "Heute" } if day.date == tomorrow { return "Morgen" } return day.wday ?? day.date } private var today: String { let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd" return f.string(from: .now) } private var tomorrow: String { let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd" let d = Calendar.current.date(byAdding: .day, value: 1, to: .now) ?? .now return f.string(from: d) } private var weatherSymbol: String { switch day.weathercode ?? 0 { case 0: return "sun.max.fill" case 1, 2: return "cloud.sun.fill" case 3: return "cloud.fill" case 45, 48: return "cloud.fog.fill" case 51...57: return "cloud.drizzle.fill" case 61...67: return "cloud.rain.fill" case 71...77: return "cloud.snow.fill" case 80...82: return "cloud.heavyrain.fill" case 95...99: return "cloud.bolt.rain.fill" default: return "cloud" } } var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 12) { Image(systemName: weatherSymbol) .font(.title2) .foregroundStyle(Color.accentColor) .frame(width: 36) VStack(alignment: .leading, spacing: 2) { Text(dateLabel).font(.headline) if let desc = day.desc { Text(desc).font(.caption).foregroundStyle(.secondary) } } Spacer() tempColumn } if !tips.isEmpty { VStack(alignment: .leading, spacing: 4) { ForEach(Array(tips.enumerated()), id: \.offset) { _, tip in Label(tip.text, systemImage: tip.icon) .font(.caption) .foregroundStyle(tip.color) } } .padding(.leading, 48) } } .padding(.vertical, 4) } private var tempColumn: some View { HStack(alignment: .firstTextBaseline, spacing: 6) { if let max = day.tempMax { Text("\(Int(max.rounded()))°").font(.headline.monospacedDigit()) } if let min = day.tempMin { Text("\(Int(min.rounded()))°") .font(.subheadline.monospacedDigit()) .foregroundStyle(.secondary) } } } private struct Tip { let text: String let icon: String let color: Color } private var tips: [Tip] { var result: [Tip] = [] if let max = day.tempMax { if max >= 30 { result.append(Tip(text: "Extreme Hitze — Gassi nur früh morgens/abends", icon: "sun.dust.fill", color: .red)) } else if max >= 25 { result.append(Tip(text: "Warm — Pfoten auf Asphalt prüfen", icon: "thermometer.sun", color: .orange)) } else if max <= 0 { result.append(Tip(text: "Frost — Pfoten nach Streusalz abwischen", icon: "snowflake", color: .blue)) } } if let asphalt = day.asphaltTemp, asphalt >= 50 { result.append(Tip(text: "Asphalt ~\(Int(asphalt.rounded()))°C — verbrennungsgefahr", icon: "flame.fill", color: .red)) } if let zecken = day.zecken, zecken == "hoch" { result.append(Tip(text: "Hohe Zecken-Gefahr", icon: "ant.fill", color: .orange)) } if let pp = day.precipProb, pp >= 70 { result.append(Tip(text: "Regen wahrscheinlich (\(pp) %)", icon: "umbrella.fill", color: .blue)) } return result } }