SPB Git

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%
4.4 KB · 117 lines swift
Raw Blame History
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