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.2 KB · 97 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// API.swift : client HTTP vers l'API de production (www.lou-ka.com)5// -----------------------------------------------------------------------------6import Foundation78struct ListingFilters: Equatable {9    var q = ""10    var city = ""11    var sector = ""12    var unitType = ""13    var source = ""14    var priceMin: Int?15    var priceMax: Int?16    /// nil = peu importe · 0 = maintenant · 30/60/90 = d'ici n jours17    var dispoDays: Int?18    var petsOk = false19    /// nil = peu importe · true = meublé · false = non meublé20    var furnished: Bool?21    var areaMin: Int?2223    var activeCount: Int {24        [q, city, sector, unitType, source].filter { !$0.isEmpty }.count25            + (priceMin != nil ? 1 : 0) + (priceMax != nil ? 1 : 0)26            + (dispoDays != nil ? 1 : 0) + (petsOk ? 1 : 0)27            + (furnished != nil ? 1 : 0) + (areaMin != nil ? 1 : 0)28    }29}3031enum APIError: LocalizedError {32    case badStatus(Int)3334    var errorDescription: String? {35        switch self {36        case .badStatus(let code): return "L'API a répondu \(code). Réessayez dans un instant."37        }38    }39}4041enum API {42    static let base = URL(string: "https://www.lou-ka.com")!4344    /// Date ISO à +n jours (paramètre `available_by`)45    static func isoInDays(_ n: Int) -> String {46        let d = Calendar.current.date(byAdding: .day, value: n, to: Date()) ?? Date()47        let f = DateFormatter()48        f.dateFormat = "yyyy-MM-dd"49        f.locale = Locale(identifier: "en_US_POSIX")50        return f.string(from: d)51    }5253    static func get<T: Decodable>(_ path: String, query: [URLQueryItem] = []) async throws -> T {54        var comps = URLComponents(url: base.appendingPathComponent(path), resolvingAgainstBaseURL: false)!55        if !query.isEmpty { comps.queryItems = query }56        var req = URLRequest(url: comps.url!)57        req.timeoutInterval = 2058        req.setValue("LouKa-iOS/1.2", forHTTPHeaderField: "User-Agent")59        let (data, resp) = try await URLSession.shared.data(for: req)60        guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {61            throw APIError.badStatus((resp as? HTTPURLResponse)?.statusCode ?? -1)62        }63        return try JSONDecoder().decode(T.self, from: data)64    }6566    static func listings(_ f: ListingFilters, limit: Int? = nil) async throws -> ListingsResponse {67        var q: [URLQueryItem] = []68        if !f.q.isEmpty { q.append(.init(name: "q", value: f.q)) }69        if !f.city.isEmpty { q.append(.init(name: "city", value: f.city)) }70        if !f.sector.isEmpty { q.append(.init(name: "sector", value: f.sector)) }71        if !f.unitType.isEmpty { q.append(.init(name: "unit_type", value: f.unitType)) }72        if !f.source.isEmpty { q.append(.init(name: "source", value: f.source)) }73        if let min = f.priceMin { q.append(.init(name: "price_min", value: String(min))) }74        if let max = f.priceMax { q.append(.init(name: "price_max", value: String(max))) }75        if f.petsOk { q.append(.init(name: "pets", value: "oui")) }76        if let furn = f.furnished { q.append(.init(name: "furnished", value: furn ? "1" : "0")) }77        if let days = f.dispoDays { q.append(.init(name: "available_by", value: isoInDays(days))) }78        if let area = f.areaMin { q.append(.init(name: "area_min", value: String(area))) }79        if let limit { q.append(.init(name: "limit", value: String(limit))) }80        return try await get("api/listings", query: q)81    }8283    static func listing(uid: String) async throws -> Listing {84        try await get("api/listings/\(uid)")85    }8687    static func facets(city: String? = nil) async throws -> Facets {88        var q: [URLQueryItem] = []89        if let city, !city.isEmpty { q.append(.init(name: "city", value: city)) }90        return try await get("api/facets", query: q)91    }9293    static func sources() async throws -> SourcesResponse { try await get("api/sources") }94    static func stats() async throws -> Stats { try await get("api/stats") }95    static func detailedStats() async throws -> DetailedStats { try await get("api/stats/detailed") }96}97