segregate files
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import SwiftUI
|
||||
|
||||
extension StoreDetailView {
|
||||
func heroIconButton(icon: String, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(Color.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(Color.white.opacity(0.24))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
func statItem(title: String, value: String) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Text(title)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Text(value)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
var ratingChip: some View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color(hex: "#F5B335"))
|
||||
Text(String(format: "%.1f", storeRating ?? 0))
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(AppColors.brandSoft)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
var storeSubtitle: String {
|
||||
let category = (storeCategory ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if category.isEmpty { return "Restaurant" }
|
||||
return category
|
||||
}
|
||||
|
||||
var deliveryValueLabel: String {
|
||||
if let minOrder = info?.minOrder {
|
||||
return formatCurrency(minOrder)
|
||||
}
|
||||
if let storeDistance, storeDistance.isEmpty == false {
|
||||
return storeDistance
|
||||
}
|
||||
return "R$ --"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadStoreData() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
async let infoRequest = ApiService().storeInfo(storeId: storeId)
|
||||
async let catalogRequest = ApiService().storeCatalog(storeId: storeId)
|
||||
let (infoResponse, catalogResponse) = try await (infoRequest, catalogRequest)
|
||||
|
||||
if infoResponse.error {
|
||||
errorMessage = infoResponse.message ?? "Não foi possível carregar a loja."
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
if catalogResponse.error {
|
||||
errorMessage = catalogResponse.message ?? "Não foi possível carregar o catálogo."
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
info = infoResponse.result
|
||||
categories = catalogResponse.result ?? []
|
||||
selectedCategoryId = categories.first?.id
|
||||
isLoading = false
|
||||
} catch {
|
||||
isLoading = false
|
||||
if let network = error as? NetworkError {
|
||||
errorMessage = network.errorDescription ?? "Erro ao carregar loja."
|
||||
return
|
||||
}
|
||||
if let service = error as? ApiServiceError {
|
||||
errorMessage = service.errorDescription ?? "Erro ao carregar loja."
|
||||
return
|
||||
}
|
||||
errorMessage = "Erro ao carregar loja."
|
||||
}
|
||||
}
|
||||
|
||||
func resolvedURL(_ raw: String?) -> String? {
|
||||
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
raw.isEmpty == false else { return nil }
|
||||
|
||||
let lower = raw.lowercased()
|
||||
if lower.hasPrefix("http://") || lower.hasPrefix("https://") {
|
||||
return raw
|
||||
}
|
||||
let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
||||
return "\(base)\(path)"
|
||||
}
|
||||
|
||||
func formatCurrency(_ value: Double?) -> String {
|
||||
guard let value else { return "R$ --" }
|
||||
return String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
||||
}
|
||||
|
||||
var topSectionHeight: CGFloat {
|
||||
cardTopInset + summaryCardHeight
|
||||
}
|
||||
|
||||
func sectionAnchorId(for categoryId: String) -> String {
|
||||
"category-section-\(categoryId)"
|
||||
}
|
||||
|
||||
func syncCategoryWithScroll() {
|
||||
guard isLoading == false else { return }
|
||||
guard isProgrammaticCategoryScroll == false else { return }
|
||||
guard categoryHeaderOffsets.isEmpty == false else { return }
|
||||
|
||||
// Section whose header is nearest to the top content area wins.
|
||||
let topThreshold: CGFloat = 180
|
||||
let sorted = categoryHeaderOffsets.sorted { $0.value < $1.value }
|
||||
|
||||
if let current = sorted.last(where: { $0.value <= topThreshold })?.key {
|
||||
selectedCategoryId = current
|
||||
return
|
||||
}
|
||||
|
||||
if let firstVisible = sorted.first?.key {
|
||||
selectedCategoryId = firstVisible
|
||||
}
|
||||
}
|
||||
|
||||
func quantityInCart(for productId: String) -> Int {
|
||||
appState.cart.items
|
||||
.filter { $0.storeId == storeId && $0.productId == productId }
|
||||
.reduce(0) { $0 + $1.quantity }
|
||||
}
|
||||
|
||||
func requestAddToCart(_ item: CartItemState) {
|
||||
pendingCartAction = .add
|
||||
if shouldAskForStoreSwitch(for: storeId) {
|
||||
pendingCartItem = item
|
||||
pendingProductSheet = nil
|
||||
showSwitchStoreAlert = true
|
||||
return
|
||||
}
|
||||
applyAddToCart(item)
|
||||
}
|
||||
|
||||
func requestSetCartItem(_ item: CartItemState) {
|
||||
pendingCartAction = .set
|
||||
if shouldAskForStoreSwitch(for: storeId) && item.quantity > 0 {
|
||||
pendingCartItem = item
|
||||
pendingProductSheet = nil
|
||||
showSwitchStoreAlert = true
|
||||
return
|
||||
}
|
||||
applySetCartItem(item)
|
||||
}
|
||||
|
||||
func requestOpenProductSheet(_ product: StoreCatalogProduct) {
|
||||
pendingCartAction = .openProductSheet
|
||||
if shouldAskForStoreSwitch(for: storeId) {
|
||||
pendingCartItem = nil
|
||||
pendingProductSheet = product
|
||||
showSwitchStoreAlert = true
|
||||
return
|
||||
}
|
||||
selectedProduct = product
|
||||
}
|
||||
|
||||
func applyAddToCart(_ item: CartItemState) {
|
||||
if appState.cart.storeId == nil {
|
||||
appState.cart.storeId = storeId
|
||||
appState.cart.storeName = storeName
|
||||
}
|
||||
appState.cart.add(item: item)
|
||||
SnackbarCenter.shared.show(title: "Produto adicionado ao carrinho.", style: .success, icon: "checkmark.seal.fill", duration: 2.0)
|
||||
}
|
||||
|
||||
func applySetCartItem(_ item: CartItemState) {
|
||||
if item.quantity > 0, appState.cart.storeId == nil {
|
||||
appState.cart.storeId = storeId
|
||||
appState.cart.storeName = storeName
|
||||
}
|
||||
appState.cart.set(item: item)
|
||||
if item.quantity > 0 {
|
||||
SnackbarCenter.shared.show(title: "Carrinho atualizado.", style: .success, icon: "checkmark.seal.fill", duration: 1.8)
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: "Produto removido do carrinho.", style: .success, icon: "checkmark.seal.fill", duration: 1.8)
|
||||
}
|
||||
}
|
||||
|
||||
func currentQuantity(forCartItemId itemId: String) -> Int {
|
||||
appState.cart.items.first(where: { $0.id == itemId })?.quantity ?? 0
|
||||
}
|
||||
|
||||
func shouldAskForStoreSwitch(for targetStoreId: String) -> Bool {
|
||||
guard appState.cart.items.isEmpty == false else { return false }
|
||||
guard let currentStoreId = currentCartStoreId(),
|
||||
currentStoreId.isEmpty == false else { return false }
|
||||
return currentStoreId != targetStoreId
|
||||
}
|
||||
|
||||
func currentCartStoreId() -> String? {
|
||||
if let storeId = appState.cart.storeId, storeId.isEmpty == false {
|
||||
return storeId
|
||||
}
|
||||
return appState.cart.items.first?.storeId
|
||||
}
|
||||
}
|
||||
|
||||
enum CartAction {
|
||||
case add
|
||||
case set
|
||||
case openProductSheet
|
||||
}
|
||||
Reference in New Issue
Block a user