1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// HomeView.swift — l'accueil VIVANT : salutation selon l'heure, suggestions3// contextuelles (restos le soir, sorties le week-end, emplois le matin…) avec4// du VRAI contenu en direct, et le pouls de l'écosystème (compteurs live).5import SwiftUI67struct HomeView: View {8 @State private var featured: [(universe: Universe, items: [KAItem])] = []9 @State private var pulse: [(Universe, Int)] = []10 @State private var loading = true11 @AppStorage("ka.favUniverses") private var favUniversesRaw = ""12 @Environment(\.colorScheme) private var scheme1314 var body: some View {15 NavigationStack {16 ScrollView {17 VStack(alignment: .leading, spacing: 22) {18 header19 pulseStrip20 ForEach(featured, id: \.universe.id) { section in21 featuredSection(section.universe, section.items)22 }23 if loading && featured.isEmpty {24 ProgressView("KA prépare votre accueil…")25 .frame(maxWidth: .infinity).padding(40)26 }27 disclaimer28 }29 .padding(16)30 }31 .background(KATheme.paper(scheme))32 .navigationTitle("")33 .toolbar {34 ToolbarItem(placement: .topBarLeading) {35 GroupeKAMark(size: 16)36 }37 }38 .navigationDestination(for: KAItem.self) { ItemDetailView(item: $0) }39 .navigationDestination(for: String.self) { id in40 if let u = Ecosystem.universe(id) { UniverseHomeView(universe: u) }41 }42 .refreshable { await load() }43 .task { await load() }44 }45 }4647 // MARK: sous-vues4849 private var greeting: (title: String, sub: String) {50 let h = Calendar.current.component(.hour, from: Date())51 let weekend = Calendar.current.isDateInWeekend(Date())52 switch h {53 case 5..<12: return ("Bon matin ☀️", weekend ? "On planifie la fin de semaine ?" : "L'écosystème a travaillé toute la nuit.")54 case 12..<17: return ("Bon après-midi 👋", "Tout le Québec, dans votre poche.")55 case 17..<22: return ("Bonsoir 🌆", weekend ? "Resto ? Sortie ? KA a des idées." : "On soupe où ce soir ?")56 default: return ("Bonne nuit 🌙", "Les connecteurs veillent — revenez demain matin.")57 }58 }5960 private var header: some View {61 VStack(alignment: .leading, spacing: 5) {62 Text(greeting.title).font(.system(.largeTitle, design: .rounded).weight(.bold))63 Text(greeting.sub).font(.subheadline).foregroundStyle(KATheme.ink2(scheme))64 }65 }6667 private var pulseStrip: some View {68 ScrollView(.horizontal, showsIndicators: false) {69 HStack(spacing: 10) {70 ForEach(pulse, id: \.0.id) { (u, n) in71 NavigationLink(value: u.id) {72 VStack(alignment: .leading, spacing: 3) {73 Text(n.formatted(.number.locale(Locale(identifier: "fr_CA"))))74 .font(.system(.headline, design: .rounded).weight(.bold))75 .foregroundStyle(u.accent)76 .contentTransition(.numericText())77 Text(u.unit).font(.caption2).foregroundStyle(.secondary)78 }79 .padding(.horizontal, 13).padding(.vertical, 9)80 .kaCard(accent: u.accent)81 }82 .buttonStyle(.plain)83 }84 if pulse.isEmpty {85 Text("Le pouls de l'écosystème…").font(.caption).foregroundStyle(.tertiary).padding(10)86 }87 }88 }89 .accessibilityLabel("Le pouls de l'écosystème en direct")90 }9192 private func featuredSection(_ u: Universe, _ items: [KAItem]) -> some View {93 VStack(alignment: .leading, spacing: 10) {94 HStack {95 KAWordmark(universe: u, size: 17)96 Spacer()97 NavigationLink(value: u.id) {98 Text("Tout voir").font(.caption.weight(.semibold)).foregroundStyle(u.accent)99 }100 }101 ScrollView(.horizontal, showsIndicators: false) {102 HStack(spacing: 12) {103 ForEach(items.prefix(8)) { item in104 NavigationLink(value: item) {105 VStack(alignment: .leading, spacing: 6) {106 Text(item.title).font(.subheadline.weight(.semibold))107 .lineLimit(2, reservesSpace: true)108 .multilineTextAlignment(.leading)109 if let s = item.subtitle {110 Text(s).font(.caption).foregroundStyle(.secondary).lineLimit(1)111 }112 Spacer(minLength: 0)113 HStack {114 if let p = item.priceLabel {115 Text(p).font(.system(.caption, design: .rounded).weight(.bold))116 .foregroundStyle(u.accent)117 }118 Spacer()119 if let c = item.city { Text(c).font(.caption2).foregroundStyle(.tertiary) }120 }121 }122 .padding(12)123 .frame(width: 190, height: 110, alignment: .topLeading)124 .kaCard(accent: u.accent)125 }126 .buttonStyle(.plain)127 }128 }129 .padding(.vertical, 4)130 }131 }132 }133134 private var disclaimer: some View {135 Text(Ecosystem.disclaimer)136 .font(.caption2).foregroundStyle(.tertiary)137 .padding(.top, 8)138 }139140 // MARK: données141142 /// Univers mis en avant selon l'heure + les préférences d'onboarding.143 private var contextualUniverses: [Universe] {144 let h = Calendar.current.component(.hour, from: Date())145 let weekend = Calendar.current.isDateInWeekend(Date())146 var ids: [String]147 switch h {148 case 5..<11: ids = ["job-ka", "food-ka", "immo-ka"]149 case 11..<14: ids = ["resto-ka", "food-ka", "sorti-ka"]150 case 14..<17: ids = weekend ? ["sorti-ka", "resto-ka", "auto-ka"] : ["immo-ka", "auto-ka", "lou-ka"]151 case 17..<23: ids = ["resto-ka", "sorti-ka", "crea-ka"]152 default: ids = ["sorti-ka", "crea-ka", "lou-ka"]153 }154 let favs = favUniversesRaw.split(separator: ",").map(String.init)155 for f in favs.reversed() where !ids.contains(f) { ids.insert(f, at: 0) }156 return ids.prefix(4).compactMap(Ecosystem.universe).filter { $0.map != nil }157 }158159 private func load() async {160 loading = true161 let universes = contextualUniverses162 var found: [String: [KAItem]] = [:]163 await withTaskGroup(of: (String, [KAItem]).self) { group in164 for u in universes {165 group.addTask { (u.id, (try? await UniverseService.fetch(u, limit: 8)) ?? []) }166 }167 for u in Ecosystem.all where u.statTotalKeys.first != nil {168 _ = u // pouls géré plus bas169 }170 for await (id, items) in group { found[id] = items }171 }172 featured = universes.compactMap { u in173 let items = found[u.id] ?? []174 return items.isEmpty ? nil : (u, items)175 }176 loading = false177 // pouls (compteurs live) — après le contenu pour ne pas retarder l'accueil178 var counts: [(Universe, Int)] = []179 await withTaskGroup(of: (String, Int?).self) { group in180 for u in Ecosystem.all {181 group.addTask { (u.id, await UniverseService.liveTotal(u)) }182 }183 for await (id, n) in group {184 if let n, let u = Ecosystem.universe(id) { counts.append((u, n)) }185 }186 }187 pulse = counts.sorted { $0.1 > $1.1 }188 }189}190