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// 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