SPB Git

spb/ka-ios

Public
8.2 KB · 190 lines swift
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
Files · 94a2a56dd85a861c4319a2f53a8841ad0481989d
Menu

Theme