This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -40,6 +40,13 @@ struct HomeView: View {
.padding(.top, headerExpandedHeight + contentTopSpacing)
.padding(.bottom, contentBottomSpacing)
}
.refreshable {
await bootstrapStoresFlow(
forceLocationRefresh: true,
category: selectedCategory == "all" ? nil : selectedCategory,
refreshCategories: true
)
}
.background(scrollOffsetObserver)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
@@ -73,6 +80,20 @@ struct HomeView: View {
}
collapseBaseOffset = scrollOffset
}
.onChange(of: addressCacheScope) { _, _ in
guard hasRequestedLocation else { return }
Task {
AppContentCache.shared.invalidate(prefix: "stores:")
AppContentCache.shared.invalidate(prefix: "store-info:")
AppContentCache.shared.invalidate(prefix: "store-catalog:")
AppImageCache.shared.invalidateAll()
await bootstrapStoresFlow(
forceLocationRefresh: true,
category: selectedCategory == "all" ? nil : selectedCategory,
refreshCategories: true
)
}
}
}
private var contentStack: some View {
@@ -309,18 +330,32 @@ struct HomeView: View {
}
}
private var sortedStores: [StoreSummary] {
private var storesByPositiveReviews: [StoreSummary] {
stores.sorted { lhs, rhs in
(lhs.distance ?? .greatestFiniteMagnitude) < (rhs.distance ?? .greatestFiniteMagnitude)
let lhsPositive = lhs.positiveReviews ?? lhs.reviewsCount ?? 0
let rhsPositive = rhs.positiveReviews ?? rhs.reviewsCount ?? 0
if lhsPositive != rhsPositive {
return lhsPositive > rhsPositive
}
let lhsRating = lhs.rating ?? 0
let rhsRating = rhs.rating ?? 0
if lhsRating != rhsRating {
return lhsRating > rhsRating
}
return (lhs.distance ?? .greatestFiniteMagnitude) < (rhs.distance ?? .greatestFiniteMagnitude)
}
}
private var featuredStoresCards: [FeaturedStoreCardModel] {
Array(sortedStores.prefix(6)).map(mapStoreToCard)
Array(storesByPositiveReviews.prefix(5)).map(mapStoreToCard)
}
private var nearbyStoreCards: [FeaturedStoreCardModel] {
Array(sortedStores.prefix(20)).map(mapStoreToCard)
let featuredIds = Set(storesByPositiveReviews.prefix(5).map(\.id))
let remaining = storesByPositiveReviews.filter { featuredIds.contains($0.id) == false }
return Array(remaining.prefix(20)).map(mapStoreToCard)
}
private func mapStoreToCard(_ store: StoreSummary) -> FeaturedStoreCardModel {
@@ -330,7 +365,7 @@ struct HomeView: View {
id: store.id,
name: store.name,
rating: store.rating ?? 0,
reviews: "0",
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
distance: formatDistance(store.distance),
category: store.category ?? "Loja",
promoText: nil,
@@ -338,20 +373,24 @@ struct HomeView: View {
iconName: "storefront",
imageURL: coverURL ?? logoURL,
logoURL: logoURL,
coverURL: coverURL
coverURL: coverURL,
isOpen: store.isOpen ?? true,
statusLabel: store.statusLabel
)
}
private func resolveStoreMediaURL(_ raw: String?) -> String? {
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
raw.isEmpty == false else { return nil }
guard var normalized = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
normalized.isEmpty == false else { return nil }
if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
return raw
normalized = normalized.replacingOccurrences(of: "\\/", with: "/")
let lower = normalized.lowercased()
if lower.hasPrefix("http://") || lower.hasPrefix("https://") || lower.hasPrefix("data:image") {
return normalized
}
let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
let path = normalized.hasPrefix("/") ? normalized : "/\(normalized)"
return "\(base)\(path)"
}
@@ -385,6 +424,26 @@ struct HomeView: View {
}
do {
let storesCacheKey = homeStoresCacheKey(
lat: coordinate?.0,
lng: coordinate?.1,
category: category
)
if forceLocationRefresh == false,
let cachedStores: [StoreSummary] = AppContentCache.shared.value(for: storesCacheKey, as: [StoreSummary].self) {
isLoadingStores = false
stores = cachedStores
if refreshCategories || (category == nil && categories.count <= 1) {
await loadHomeCategories(withFallbackStores: cachedStores)
if categories.contains(where: { $0.id == selectedCategory }) == false {
selectedCategory = "all"
}
}
storesError = nil
return
}
let response = try await ApiService().listStores(
lat: coordinate?.0,
lng: coordinate?.1,
@@ -398,6 +457,7 @@ struct HomeView: View {
}
let results = response.result ?? []
stores = results
AppContentCache.shared.set(results, for: storesCacheKey, ttl: 180)
if refreshCategories || (category == nil && categories.count <= 1) {
await loadHomeCategories(withFallbackStores: results)
if categories.contains(where: { $0.id == selectedCategory }) == false {
@@ -412,7 +472,6 @@ struct HomeView: View {
}
}
@ViewBuilder
private var scrollOffsetObserver: some View {
#if os(iOS)
@@ -427,4 +486,19 @@ struct HomeView: View {
EmptyView()
#endif
}
private var addressCacheScope: String {
let selected = appState.address.selectedId ?? "nil"
let display = appState.address.display
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return "\(selected)|\(display)"
}
private func homeStoresCacheKey(lat: Double?, lng: Double?, category: String?) -> String {
let normalizedCategory = category?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "all"
let latKey = lat.map { String(format: "%.4f", $0) } ?? "nil"
let lngKey = lng.map { String(format: "%.4f", $0) } ?? "nil"
return "stores:\(latKey):\(lngKey):\(normalizedCategory)"
}
}