SPB Git

spb/ka-ios

Public
7.4 KB · 168 lines swift
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// SearchView.swift — la recherche UNIVERSELLE : une seule barre qui interroge3// tous les univers en parallèle (fan-out sur leurs API + web québécois via4// Trouve·Ka), résultats mélangés groupés par univers, filtres par univers,5// historique local.6import SwiftUI78struct SearchView: View {9    @State private var query = ""10    @State private var submitted = ""11    @State private var sections: [(universe: Universe, items: [KAItem])] = []12    @State private var searching = false13    @State private var selected: Set<String> = []       // filtres univers (vide = tous)14    @AppStorage("ka.search.history") private var historyRaw = ""15    @Environment(\.colorScheme) private var scheme1617    private var history: [String] { historyRaw.split(separator: "\n").map(String.init) }18    private var searchables: [Universe] { Ecosystem.all.filter { $0.map != nil } }1920    var body: some View {21        NavigationStack {22            ScrollView {23                LazyVStack(alignment: .leading, spacing: 16) {24                    filterChips25                    if searching {26                        HStack(spacing: 10) {27                            ProgressView()28                            Text("KA interroge \(selected.isEmpty ? searchables.count : selected.count) univers…")29                                .font(.subheadline).foregroundStyle(.secondary)30                        }31                        .padding(24).frame(maxWidth: .infinity)32                    } else if submitted.isEmpty {33                        suggestions34                    } else if sections.allSatisfy({ $0.items.isEmpty }) {35                        KAEmptyState(symbol: "magnifyingglass", title: "Rien trouvé pour « \(submitted) »",36                                     message: "Essayez un autre mot, ou élargissez les univers filtrés.")37                    } else {38                        results39                    }40                }41                .padding(16)42            }43            .background(KATheme.paper(scheme))44            .navigationTitle("Recherche")45            .searchable(text: $query, prompt: "appartement Rouyn, resto italien, Corolla…")46            .onSubmit(of: .search) { Task { await search() } }47            .navigationDestination(for: KAItem.self) { ItemDetailView(item: $0) }48        }49    }5051    private var filterChips: some View {52        ScrollView(.horizontal, showsIndicators: false) {53            HStack(spacing: 8) {54                ForEach(searchables) { u in55                    let on = selected.isEmpty || selected.contains(u.id)56                    Button {57                        Haptics.tap()58                        if selected.contains(u.id) { selected.remove(u.id) }59                        else { selected.insert(u.id) }60                        if selected.count == searchables.count { selected = [] }61                        if !submitted.isEmpty { Task { await search(submitted) } }62                    } label: {63                        Text(u.wordmark)64                            .font(.system(.caption, design: .monospaced).weight(.bold))65                            .padding(.horizontal, 11).padding(.vertical, 7)66                            .background(on ? u.accent.opacity(0.9) : KATheme.surface(scheme), in: Capsule())67                            .foregroundStyle(on ? .white : .secondary)68                            .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(0.3), lineWidth: 1))69                    }70                    .accessibilityLabel("\(on ? "Masquer" : "Afficher") \(u.name)")71                }72            }73        }74    }7576    private var suggestions: some View {77        VStack(alignment: .leading, spacing: 14) {78            if !history.isEmpty {79                Text("Récemment cherché").font(.headline)80                ForEach(history.prefix(6), id: \.self) { h in81                    Button {82                        query = h83                        Task { await search(h) }84                    } label: {85                        HStack {86                            Image(systemName: "clock.arrow.circlepath").foregroundStyle(.secondary)87                            Text(h)88                            Spacer()89                        }90                        .padding(12).kaCard()91                    }92                    .buttonStyle(.plain)93                }94            }95            Text("Idées").font(.headline)96            ForEach(["4½ à Québec", "resto italien Montréal", "Toyota Corolla", "emploi infirmière", "spectacle ce week-end"], id: \.self) { s in97                Button { query = s; Task { await search(s) } } label: {98                    HStack {99                        Image(systemName: "sparkle.magnifyingglass").foregroundStyle(KATheme.green)100                        Text(s); Spacer()101                    }102                    .padding(12).kaCard()103                }104                .buttonStyle(.plain)105            }106        }107    }108109    private var results: some View {110        ForEach(sections.filter { !$0.items.isEmpty }, id: \.universe.id) { section in111            VStack(alignment: .leading, spacing: 10) {112                HStack {113                    KAWordmark(universe: section.universe, size: 16)114                    Spacer()115                    Text("\(section.items.count)")116                        .font(.system(.caption, design: .monospaced).weight(.bold))117                        .foregroundStyle(section.universe.accent)118                }119                ForEach(section.items.prefix(5)) { item in120                    NavigationLink(value: item) { KAItemRow(item: item) }121                        .buttonStyle(.plain)122                }123                NavigationLink(value: section.universe.id) {124                    Text("Tout voir dans \(section.universe.name) →")125                        .font(.subheadline.weight(.semibold))126                        .foregroundStyle(section.universe.accent)127                }128            }129        }130        .navigationDestination(for: String.self) { id in131            if let u = Ecosystem.universe(id) { UniverseHomeView(universe: u) }132        }133    }134135    private func search(_ text: String? = nil) async {136        let q = (text ?? query).trimmingCharacters(in: .whitespaces)137        guard !q.isEmpty else { return }138        submitted = q139        searching = true140        Haptics.rigid()141        var hist = history.filter { $0 != q }142        hist.insert(q, at: 0)143        historyRaw = hist.prefix(10).joined(separator: "\n")144145        let targets = searchables.filter { selected.isEmpty || selected.contains($0.id) }146        var found: [String: [KAItem]] = [:]147        await withTaskGroup(of: (String, [KAItem]).self) { group in148            for u in targets {149                group.addTask {150                    let items = (try? await UniverseService.fetch(u, query: q, limit: 8)) ?? []151                    return (u.id, items)152                }153            }154            for await (id, items) in group { found[id] = items }155        }156        // ordre : univers avec le plus de résultats d'abord, web québécois à la fin157        sections = targets158            .map { ($0, found[$0.id] ?? []) }159            .sorted {160                if $0.0.id == "trouve-ka" { return false }161                if $1.0.id == "trouve-ka" { return true }162                return $0.1.count > $1.1.count163            }164        searching = false165        if sections.contains(where: { !$0.items.isEmpty }) { Haptics.success() }166    }167}168
Files · 94a2a56dd85a861c4319a2f53a8841ad0481989d
Menu

Theme