spb/lou-ka-ios Public
Lou·Ka iOS — app SwiftUI native de l'agrégateur de logements du Québec : filtres avancés, carte, stats, et mode Découverte (swipe) avec recommandation on-device
Swift 100%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// SourcesView.swift : registre des gestionnaires (statut, annonces, dernière sync)5// -----------------------------------------------------------------------------6import SwiftUI78struct SourcesView: View {9 @Environment(AppModel.self) private var model10 @State private var query = ""1112 private var actives: [SourceInfo] {13 model.sources.filter { $0.activeListings > 0 }14 }1516 private var filtered: [SourceInfo] {17 let base = model.sources.sorted {18 ($0.activeListings, $1.name) > ($1.activeListings, $0.name)19 }20 guard !query.isEmpty else { return base }21 return base.filter {22 $0.name.localizedCaseInsensitiveContains(query)23 || $0.id.localizedCaseInsensitiveContains(query)24 }25 }2627 var body: some View {28 NavigationStack {29 ScrollView {30 VStack(alignment: .leading, spacing: 14) {31 Kicker(text: "Registre des gestionnaires")32 HStack(spacing: 10) {33 counter(value: model.sources.count, label: "sources recensées")34 counter(value: actives.count, label: "avec annonces")35 }36 ForEach(filtered) { source in37 row(source)38 }39 }40 .padding(16)41 .padding(.bottom, 24)42 }43 .background(LK.paper)44 .navigationTitle("Sources")45 .navigationBarTitleDisplayMode(.inline)46 .searchable(text: $query, prompt: "Chercher un gestionnaire")47 .refreshable { await model.loadGlobals(force: true) }48 .overlay {49 if model.sources.isEmpty {50 ProgressView("Chargement…")51 }52 }53 }54 }5556 private func counter(value: Int, label: String) -> some View {57 VStack(alignment: .leading, spacing: 2) {58 Text(Fmt.int(value))59 .font(LKFont.display(24, .bold))60 Text(label.uppercased())61 .font(LKFont.mono(9, .medium))62 .kerning(0.5)63 .foregroundStyle(LK.ink3)64 }65 .padding(12)66 .frame(maxWidth: .infinity, alignment: .leading)67 .background(LK.surface)68 .clipShape(RoundedRectangle(cornerRadius: 8))69 .overlay(RoundedRectangle(cornerRadius: 8).stroke(LK.line, lineWidth: 1))70 }7172 private func row(_ source: SourceInfo) -> some View {73 let connected = source.activeListings > 074 return HStack(spacing: 12) {75 Circle()76 .fill(connected ? LK.green : LK.amber)77 .frame(width: 8, height: 8)78 VStack(alignment: .leading, spacing: 2) {79 Text(source.name)80 .font(LKFont.display(15, .medium))81 .lineLimit(1)82 HStack(spacing: 6) {83 Text(source.id)84 .font(LKFont.mono(10))85 .foregroundStyle(LK.ink3)86 if let sync = Fmt.relative(source.lastSync) {87 Text("· sync \(sync)")88 .font(.system(size: 10.5))89 .foregroundStyle(LK.ink3)90 }91 }92 }93 Spacer()94 if connected {95 Text("\(source.activeListings)")96 .font(LKFont.mono(12, .bold))97 .padding(.horizontal, 8)98 .padding(.vertical, 4)99 .background(LK.limeSoft)100 .foregroundStyle(LK.greenDeep)101 .clipShape(Capsule())102 } else {103 Text(source.status)104 .font(LKFont.mono(9.5, .medium))105 .foregroundStyle(LK.amber)106 .lineLimit(1)107 .frame(maxWidth: 90, alignment: .trailing)108 }109 }110 .padding(.horizontal, 13)111 .padding(.vertical, 10)112 .background(LK.surface)113 .clipShape(RoundedRectangle(cornerRadius: 8))114 .overlay(RoundedRectangle(cornerRadius: 8).stroke(LK.line, lineWidth: 1))115 }116}117