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%
5.1 KB · 131 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// ListingCardView.swift : carte d'annonce (photo, prix, adresse, badges)5// -----------------------------------------------------------------------------6import SwiftUI78struct ListingCardView: View {9    @Environment(AppModel.self) private var model10    let listing: Listing1112    var body: some View {13        VStack(alignment: .leading, spacing: 0) {14            photo15            VStack(alignment: .leading, spacing: 8) {16                HStack(alignment: .firstTextBaseline, spacing: 6) {17                    Text(Fmt.price(listing.price, label: listing.priceLabel))18                        .font(LKFont.display(23, .bold))19                        .kerning(-0.5)20                        .foregroundStyle(LK.ink)21                    if listing.price != nil {22                        Text(listing.details?.priceFrom == true ? "à partir de / mois" : "/ mois")23                            .font(LKFont.mono(10.5))24                            .foregroundStyle(LK.ink3)25                    }26                    Spacer()27                }28                Text(listing.title.isEmpty ? listing.address : listing.title)29                    .font(.system(size: 15.5, weight: .semibold))30                    .foregroundStyle(LK.ink)31                    .lineLimit(2)32                HStack(spacing: 5) {33                    Image(systemName: "mappin")34                        .font(.system(size: 11))35                    Text([listing.sector, listing.city].filter { !$0.isEmpty }.joined(separator: " · "))36                        .lineLimit(1)37                }38                .font(.system(size: 13))39                .foregroundStyle(LK.ink2)4041                Rectangle().fill(LK.line).frame(height: 1).padding(.vertical, 2)4243                HStack(spacing: 8) {44                    if let dispo = Fmt.availability(listing.availabilityDate) {45                        metaPill(icon: "calendar", text: dispo, highlight: listing.availabilityDate == "now")46                    }47                    if let area = listing.areaSqft {48                        metaPill(icon: "ruler", text: "\(Int(area)) pi²", highlight: false)49                    }50                    Spacer()51                    Text(model.sourceName(listing.source).uppercased())52                        .font(LKFont.mono(9, .medium))53                        .kerning(0.5)54                        .foregroundStyle(LK.ink3)55                        .lineLimit(1)56                }57            }58            .padding(14)59        }60        .lkCard(offset: 6)61    }6263    private func metaPill(icon: String, text: String, highlight: Bool) -> some View {64        HStack(spacing: 4) {65            Image(systemName: icon).font(.system(size: 9.5, weight: .semibold))66            Text(text)67                .font(LKFont.mono(10.5, .medium))68        }69        .padding(.horizontal, 8)70        .padding(.vertical, 4)71        .background(highlight ? LK.limeSoft : LK.surface2)72        .foregroundStyle(highlight ? LK.greenDeep : LK.ink2)73        .clipShape(Capsule())74        .overlay(Capsule().stroke(highlight ? LK.green.opacity(0.4) : LK.line, lineWidth: 1))75    }7677    private var photo: some View {78        ZStack(alignment: .topLeading) {79            Group {80                if let first = listing.images.first, let url = URL(string: first) {81                    AsyncImage(url: url) { phase in82                        switch phase {83                        case .success(let image):84                            image.resizable().aspectRatio(contentMode: .fill)85                        case .failure:86                            placeholder87                        default:88                            LK.surface289                        }90                    }91                } else {92                    placeholder93                }94            }95            .frame(height: 190)96            .frame(maxWidth: .infinity)97            .clipped()9899            HStack(spacing: 6) {100                if !listing.unitType.isEmpty {101                    UnitTypeBadge(type: listing.unitType)102                }103                if listing.images.count > 1 {104                    HStack(spacing: 3) {105                        Image(systemName: "photo.stack").font(.system(size: 10))106                        Text("\(listing.images.count)")107                            .font(LKFont.mono(10.5, .medium))108                    }109                    .padding(.horizontal, 7)110                    .padding(.vertical, 4)111                    .background(.black.opacity(0.55))112                    .foregroundStyle(.white)113                    .clipShape(RoundedRectangle(cornerRadius: 5))114                }115            }116            .padding(10)117        }118        .background(LK.surface2)119        .overlay(Rectangle().fill(LK.ink).frame(height: 1.8), alignment: .bottom)120    }121122    private var placeholder: some View {123        ZStack {124            LK.limeSoft125            Image(systemName: "house.lodge")126                .font(.system(size: 36))127                .foregroundStyle(LK.green.opacity(0.5))128        }129    }130}131