backup
This commit is contained in:
@@ -14,12 +14,12 @@ struct HomeView: View {
|
||||
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil)
|
||||
]
|
||||
@State var scrollOffset: CGFloat = 0
|
||||
@State var collapseBaseOffset: CGFloat = 0
|
||||
@State var collapseDragStartOffset: CGFloat? = nil
|
||||
@State var hasRequestedLocation = false
|
||||
@State var isLoadingStores = false
|
||||
@State var storesError: String? = nil
|
||||
@State var stores: [StoreSummary] = []
|
||||
@State var productSearchIndexByStoreId: [String: [String]] = [:]
|
||||
@State var searchDebounceToken = 0
|
||||
|
||||
private let specials: [SpecialOfferCardModel] = [
|
||||
// .init(id: "ddddd", title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]),
|
||||
@@ -43,28 +43,12 @@ struct HomeView: View {
|
||||
}
|
||||
.refreshable {
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
forceNetworkRefresh: true,
|
||||
category: selectedCategoryQueryValue,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
.background(scrollOffsetObserver)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
let range = headerExpandedHeight - headerCollapsedHeight
|
||||
if collapseDragStartOffset == nil {
|
||||
collapseDragStartOffset = collapseBaseOffset
|
||||
}
|
||||
let start = collapseDragStartOffset ?? collapseBaseOffset
|
||||
let candidate = start - value.translation.height
|
||||
scrollOffset = clamp(value: candidate, lower: 0, upper: range)
|
||||
}
|
||||
.onEnded { _ in
|
||||
collapseBaseOffset = scrollOffset
|
||||
collapseDragStartOffset = nil
|
||||
}
|
||||
)
|
||||
|
||||
header(collapseProgress: collapseProgress, height: headerHeight)
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
@@ -79,18 +63,24 @@ struct HomeView: View {
|
||||
await bootstrapStoresFlow(refreshCategories: true)
|
||||
}
|
||||
}
|
||||
collapseBaseOffset = scrollOffset
|
||||
}
|
||||
.onChange(of: addressCacheScope) { _, _ in
|
||||
guard hasRequestedLocation else { return }
|
||||
Task {
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
category: selectedCategoryQueryValue,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _, _ in
|
||||
scheduleSearchIndexUpdate()
|
||||
}
|
||||
.onChange(of: appState.homeFilters.sortOption) { _, _ in scheduleSearchIndexUpdate() }
|
||||
.onChange(of: appState.homeFilters.selectedCategories) { _, _ in scheduleSearchIndexUpdate() }
|
||||
.onChange(of: appState.homeFilters.selectedPriceTier) { _, _ in scheduleSearchIndexUpdate() }
|
||||
.onChange(of: appState.homeFilters.maxDistanceKm) { _, _ in scheduleSearchIndexUpdate() }
|
||||
}
|
||||
|
||||
private var contentStack: some View {
|
||||
@@ -153,7 +143,7 @@ struct HomeView: View {
|
||||
Task {
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
category: selectedCategoryQueryValue,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
@@ -162,14 +152,14 @@ struct HomeView: View {
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
} else if nearbyStoreCards.isEmpty {
|
||||
Text("Nenhum estabelecimento encontrado próximo à sua localização.")
|
||||
} else if filteredStoreCards.isEmpty {
|
||||
Text(emptyResultMessage)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.horizontal, 20)
|
||||
} else {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(nearbyStoreCards) { store in
|
||||
ForEach(filteredStoreCards) { store in
|
||||
NavigationLink {
|
||||
StoreDetailView(
|
||||
storeId: store.id,
|
||||
@@ -275,6 +265,9 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
SearchBar(placeholder: "Search menu, restaurant or craving", text: $searchText) {
|
||||
appState.homeFilters.availableCategories = categories
|
||||
.filter { $0.id.lowercased() != "all" }
|
||||
.map(\.title)
|
||||
appState.activeModal = .filters
|
||||
}
|
||||
.offset(y: collapseProgress * -120)
|
||||
@@ -315,7 +308,7 @@ struct HomeView: View {
|
||||
guard category.id != selectedCategory else { return }
|
||||
selectedCategory = category.id
|
||||
Task {
|
||||
await bootstrapStoresFlow(category: category.id == "all" ? nil : category.id)
|
||||
await bootstrapStoresFlow(category: category.id.lowercased() == "all" ? nil : category.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,77 +331,14 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var storesByPositiveReviews: [StoreSummary] {
|
||||
stores.sorted { lhs, rhs in
|
||||
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(storesByPositiveReviews.prefix(5)).map(mapStoreToCard)
|
||||
}
|
||||
|
||||
private var nearbyStoreCards: [FeaturedStoreCardModel] {
|
||||
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 {
|
||||
let coverURL = resolveStoreMediaURL(store.cover)
|
||||
let logoURL = resolveStoreMediaURL(store.logo)
|
||||
return FeaturedStoreCardModel(
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
rating: store.rating ?? 0,
|
||||
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
||||
distance: formatDistance(store.distance),
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||
iconName: "storefront",
|
||||
imageURL: coverURL ?? logoURL,
|
||||
logoURL: logoURL,
|
||||
coverURL: coverURL,
|
||||
isOpen: store.isOpen ?? true,
|
||||
statusLabel: store.statusLabel
|
||||
)
|
||||
}
|
||||
|
||||
private func resolveStoreMediaURL(_ raw: String?) -> String? {
|
||||
ImageSourceResolver.resolve(raw)
|
||||
}
|
||||
|
||||
private var profilePictureURL: String? {
|
||||
resolveStoreMediaURL(appState.profile.profilePicture)
|
||||
}
|
||||
|
||||
private func formatDistance(_ distance: Double?) -> String {
|
||||
guard let distance else { return "Distância indisponível" }
|
||||
if distance >= 1 {
|
||||
return String(format: "%.1f km", distance)
|
||||
}
|
||||
return "\(Int(distance * 1000)) m"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStoresFlow(
|
||||
forceLocationRefresh: Bool = false,
|
||||
forceNetworkRefresh: Bool = false,
|
||||
category: String? = nil,
|
||||
refreshCategories: Bool = false
|
||||
) async {
|
||||
if isLoadingStores { return }
|
||||
isLoadingStores = true
|
||||
storesError = nil
|
||||
|
||||
@@ -432,6 +362,7 @@ struct HomeView: View {
|
||||
)
|
||||
|
||||
if forceLocationRefresh == false,
|
||||
forceNetworkRefresh == false,
|
||||
let cachedStores: [StoreSummary] = AppContentCache.shared.value(for: storesCacheKey, as: [StoreSummary].self) {
|
||||
isLoadingStores = false
|
||||
stores = cachedStores
|
||||
@@ -445,7 +376,10 @@ struct HomeView: View {
|
||||
#endif
|
||||
AppContentCache.shared.set(cachedStores, for: AppCacheKey.homeStoresLatestSnapshot, ttl: AppCacheTTL.twoHours)
|
||||
if refreshCategories || (category == nil && categories.count <= 1) {
|
||||
await loadHomeCategories(withFallbackStores: cachedStores, forceRefresh: forceLocationRefresh)
|
||||
await loadHomeCategories(
|
||||
withFallbackStores: cachedStores,
|
||||
forceRefresh: forceLocationRefresh || forceNetworkRefresh
|
||||
)
|
||||
if categories.contains(where: { $0.id == selectedCategory }) == false {
|
||||
selectedCategory = "all"
|
||||
}
|
||||
@@ -478,13 +412,20 @@ struct HomeView: View {
|
||||
AppContentCache.shared.set(results, for: storesCacheKey, ttl: AppCacheTTL.homeStores)
|
||||
AppContentCache.shared.set(results, for: AppCacheKey.homeStoresLatestSnapshot, ttl: AppCacheTTL.twoHours)
|
||||
if refreshCategories || (category == nil && categories.count <= 1) {
|
||||
await loadHomeCategories(withFallbackStores: results, forceRefresh: forceLocationRefresh)
|
||||
await loadHomeCategories(
|
||||
withFallbackStores: results,
|
||||
forceRefresh: forceLocationRefresh || forceNetworkRefresh
|
||||
)
|
||||
if categories.contains(where: { $0.id == selectedCategory }) == false {
|
||||
selectedCategory = "all"
|
||||
}
|
||||
}
|
||||
storesError = nil
|
||||
} catch {
|
||||
if isCancelledRequest(error) {
|
||||
isLoadingStores = false
|
||||
return
|
||||
}
|
||||
isLoadingStores = false
|
||||
stores = []
|
||||
storesError = storesUserMessage(error)
|
||||
@@ -498,7 +439,6 @@ struct HomeView: View {
|
||||
// Use only upward displacement for collapse and ignore top bounce.
|
||||
let normalized = max(0, y)
|
||||
scrollOffset = normalized
|
||||
collapseBaseOffset = normalized
|
||||
}
|
||||
.frame(width: 0, height: 0)
|
||||
#else
|
||||
@@ -522,4 +462,30 @@ struct HomeView: View {
|
||||
let lngKey = lng.map { String(format: "%.4f", $0) } ?? "nil"
|
||||
return "stores:\(latKey):\(lngKey):\(normalizedCategory)"
|
||||
}
|
||||
|
||||
private var selectedCategoryQueryValue: String? {
|
||||
guard selectedCategory.lowercased() != "all" else { return nil }
|
||||
guard let selected = categories.first(where: { $0.id == selectedCategory }) else { return nil }
|
||||
return selected.title
|
||||
}
|
||||
|
||||
private func isCancelledRequest(_ error: Error) -> Bool {
|
||||
if error is CancellationError {
|
||||
return true
|
||||
}
|
||||
|
||||
if let networkError = error as? NetworkError {
|
||||
switch networkError {
|
||||
case .cancelled:
|
||||
return true
|
||||
case .transportError(let message):
|
||||
let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return normalized.contains("cancel")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return error.localizedDescription.lowercased().contains("cancel")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user