migration
This commit is contained in:
199
PediFoods/Views/Main/StoreDetailView.swift
Normal file
199
PediFoods/Views/Main/StoreDetailView.swift
Normal file
@@ -0,0 +1,199 @@
|
||||
import SwiftUI
|
||||
#if canImport(LCEssentials)
|
||||
import LCEssentials
|
||||
#endif
|
||||
import UIKit
|
||||
|
||||
struct StoreDetailView: View {
|
||||
let storeId: String
|
||||
let storeName: String
|
||||
let storeCoverURL: String?
|
||||
let storeLogoURL: String?
|
||||
let storeCategory: String?
|
||||
let storeRating: Double?
|
||||
let storeDistance: String?
|
||||
let storeDeliveryFee: Double?
|
||||
@Binding var appState: AppState
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@State var isLoading = true
|
||||
@State var errorMessage: String? = nil
|
||||
@State var info: StoreInfoResult? = nil
|
||||
@State var categories: [StoreCatalogCategory] = []
|
||||
@State var selectedCategoryId: String? = nil
|
||||
@State var selectedProduct: StoreCatalogProduct? = nil
|
||||
@State var selectedPizzaCategoryId: String? = nil
|
||||
@State var showSwitchStoreAlert = false
|
||||
@State var pendingCartItem: CartItemState? = nil
|
||||
@State var pendingProductSheet: StoreCatalogProduct? = nil
|
||||
@State var pendingPizzaCategoryId: String? = nil
|
||||
@State var pendingCartAction: CartAction = .add
|
||||
@State var didLoad = false
|
||||
@State var categoryHeaderOffsets: [String: CGFloat] = [:]
|
||||
@State var isProgrammaticCategoryScroll = false
|
||||
@State var isFavoriteRequestInFlight = false
|
||||
@State var scrollOffset: CGFloat = 0
|
||||
|
||||
var isCategoryTabsPinned: Bool { scrollOffset >= topSectionHeight }
|
||||
var stretchAmount: CGFloat { max(0, -scrollOffset) }
|
||||
|
||||
let cardTopInset: CGFloat = 180
|
||||
let summaryCardBaseHeight: CGFloat = 212
|
||||
let closedBannerHeight: CGFloat = 44
|
||||
let coverVisibleUntilY: CGFloat = 253
|
||||
let storeLogoSize: CGFloat = 84
|
||||
|
||||
var safeAreaTop: CGFloat {
|
||||
UIDevice.appSafeAreaTop
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .top) {
|
||||
ScrollView(showsIndicators: false) {
|
||||
ScrollOffsetReader(offsetY: $scrollOffset)
|
||||
LazyVStack(spacing: 0) {
|
||||
topSection
|
||||
// Guaranteed-visible refresh feedback, right below
|
||||
// the hero — not relying on the native spinner's
|
||||
// position (unreliable here, see .refreshable note
|
||||
// below).
|
||||
if isLoading && categories.isEmpty == false {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
Text("Atualizando...")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
categoryTabs(proxy: proxy, isPinned: false)
|
||||
.opacity(isCategoryTabsPinned ? 0 : 1)
|
||||
sectionedProducts
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: StoreDetailScrollCoordinateSpace.name)
|
||||
.refreshable {
|
||||
// Run the actual load in its own unstructured Task and
|
||||
// await that, instead of awaiting loadStoreData directly
|
||||
// in this closure. SwiftUI can cancel .refreshable's own
|
||||
// wrapping Task (e.g. the gesture not fully "committing")
|
||||
// independent of whether the network call is still
|
||||
// legitimately in flight. Awaiting Task.value here
|
||||
// blocks until the detached load genuinely finishes
|
||||
// (success, error, or our own 20s ApiClient timeout),
|
||||
// so a premature refreshable-cancellation can no longer
|
||||
// silently swallow a real in-flight request.
|
||||
await Task { await loadStoreData(forceRefresh: true) }.value
|
||||
}
|
||||
// NOT .ignoresSafeArea here: combined with .refreshable on
|
||||
// the same view, it breaks the native pull-to-refresh
|
||||
// spinner's positioning (renders invisible/off-place) even
|
||||
// though the gesture still fires the closure. The hero
|
||||
// image and gradient above already bleed under the status
|
||||
// bar independently via their own .ignoresSafeArea calls.
|
||||
.background(AppColors.backgroundLight)
|
||||
|
||||
categoryTabs(proxy: proxy, isPinned: true)
|
||||
.opacity(isCategoryTabsPinned ? 1 : 0)
|
||||
.allowsHitTesting(isCategoryTabsPinned)
|
||||
.zIndex(10)
|
||||
}
|
||||
.ignoresSafeArea(edges: .top)
|
||||
.saturation(isStoreOpen ? 1 : 0)
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.appHiddenNavigationBar()
|
||||
.task {
|
||||
guard didLoad == false else { return }
|
||||
didLoad = true
|
||||
await loadStoreData(forceRefresh: false)
|
||||
}
|
||||
.sheet(item: $selectedProduct) { product in
|
||||
NavigationStack {
|
||||
ProductDetailSheet(
|
||||
product: product,
|
||||
imageURL: resolvedURL(product.image),
|
||||
storeId: storeId,
|
||||
currentQuantityForItemId: { itemId in
|
||||
currentQuantity(forCartItemId: itemId)
|
||||
},
|
||||
onAdd: { item in
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
requestSetCartItem(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(
|
||||
isPresented: Binding(
|
||||
get: { selectedPizzaCategoryId != nil },
|
||||
set: { isPresented in
|
||||
if isPresented == false {
|
||||
selectedPizzaCategoryId = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
if let category = categories.first(where: { $0.id == selectedPizzaCategoryId }) {
|
||||
NavigationStack {
|
||||
PizzaProductDetailSheet(
|
||||
category: category,
|
||||
storeId: storeId,
|
||||
resolveImageURL: { raw in resolvedURL(raw) },
|
||||
currentQuantityForItemId: { itemId in
|
||||
currentQuantity(forCartItemId: itemId)
|
||||
},
|
||||
onAdd: { item in
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
requestSetCartItem(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.alert("Trocar de loja?", isPresented: $showSwitchStoreAlert) {
|
||||
Button("Cancelar", role: .cancel) {
|
||||
pendingCartItem = nil
|
||||
pendingProductSheet = nil
|
||||
pendingPizzaCategoryId = nil
|
||||
}
|
||||
Button("Limpar carrinho e adicionar", role: .destructive) {
|
||||
appState.cart.clear()
|
||||
switch pendingCartAction {
|
||||
case .add:
|
||||
guard let pendingCartItem else { return }
|
||||
applyAddToCart(pendingCartItem)
|
||||
case .set:
|
||||
guard let pendingCartItem else { return }
|
||||
applySetCartItem(pendingCartItem)
|
||||
case .openProductSheet:
|
||||
guard let pendingProductSheet else { return }
|
||||
selectedProduct = pendingProductSheet
|
||||
case .openPizzaSheet:
|
||||
guard let pendingPizzaCategoryId else { return }
|
||||
selectedPizzaCategoryId = pendingPizzaCategoryId
|
||||
}
|
||||
self.pendingCartItem = nil
|
||||
self.pendingProductSheet = nil
|
||||
self.pendingPizzaCategoryId = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Seu carrinho tem itens de outra loja. Deseja limpar o carrinho atual para adicionar este produto?")
|
||||
}
|
||||
.onPreferenceChange(CategoryHeaderOffsetPreferenceKey.self) { offsets in
|
||||
categoryHeaderOffsets = offsets
|
||||
syncCategoryWithScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user