// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // ListingCardView.swift : carte d'annonce (photo, prix, adresse, badges) // ----------------------------------------------------------------------------- import SwiftUI struct ListingCardView: View { @Environment(AppModel.self) private var model let listing: Listing var body: some View { VStack(alignment: .leading, spacing: 0) { photo VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(Fmt.price(listing.price, label: listing.priceLabel)) .font(LKFont.display(23, .bold)) .kerning(-0.5) .foregroundStyle(LK.ink) if listing.price != nil { Text(listing.details?.priceFrom == true ? "à partir de / mois" : "/ mois") .font(LKFont.mono(10.5)) .foregroundStyle(LK.ink3) } Spacer() } Text(listing.title.isEmpty ? listing.address : listing.title) .font(.system(size: 15.5, weight: .semibold)) .foregroundStyle(LK.ink) .lineLimit(2) HStack(spacing: 5) { Image(systemName: "mappin") .font(.system(size: 11)) Text([listing.sector, listing.city].filter { !$0.isEmpty }.joined(separator: " · ")) .lineLimit(1) } .font(.system(size: 13)) .foregroundStyle(LK.ink2) Rectangle().fill(LK.line).frame(height: 1).padding(.vertical, 2) HStack(spacing: 8) { if let dispo = Fmt.availability(listing.availabilityDate) { metaPill(icon: "calendar", text: dispo, highlight: listing.availabilityDate == "now") } if let area = listing.areaSqft { metaPill(icon: "ruler", text: "\(Int(area)) pi²", highlight: false) } Spacer() Text(model.sourceName(listing.source).uppercased()) .font(LKFont.mono(9, .medium)) .kerning(0.5) .foregroundStyle(LK.ink3) .lineLimit(1) } } .padding(14) } .lkCard(offset: 6) } private func metaPill(icon: String, text: String, highlight: Bool) -> some View { HStack(spacing: 4) { Image(systemName: icon).font(.system(size: 9.5, weight: .semibold)) Text(text) .font(LKFont.mono(10.5, .medium)) } .padding(.horizontal, 8) .padding(.vertical, 4) .background(highlight ? LK.limeSoft : LK.surface2) .foregroundStyle(highlight ? LK.greenDeep : LK.ink2) .clipShape(Capsule()) .overlay(Capsule().stroke(highlight ? LK.green.opacity(0.4) : LK.line, lineWidth: 1)) } private var photo: some View { ZStack(alignment: .topLeading) { Group { if let first = listing.images.first, let url = URL(string: first) { AsyncImage(url: url) { phase in switch phase { case .success(let image): image.resizable().aspectRatio(contentMode: .fill) case .failure: placeholder default: LK.surface2 } } } else { placeholder } } .frame(height: 190) .frame(maxWidth: .infinity) .clipped() HStack(spacing: 6) { if !listing.unitType.isEmpty { UnitTypeBadge(type: listing.unitType) } if listing.images.count > 1 { HStack(spacing: 3) { Image(systemName: "photo.stack").font(.system(size: 10)) Text("\(listing.images.count)") .font(LKFont.mono(10.5, .medium)) } .padding(.horizontal, 7) .padding(.vertical, 4) .background(.black.opacity(0.55)) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: 5)) } } .padding(10) } .background(LK.surface2) .overlay(Rectangle().fill(LK.ink).frame(height: 1.8), alignment: .bottom) } private var placeholder: some View { ZStack { LK.limeSoft Image(systemName: "house.lodge") .font(.system(size: 36)) .foregroundStyle(LK.green.opacity(0.5)) } } }