segregate files

This commit is contained in:
Daniel Arantes Loverde
2026-02-13 11:01:54 -03:00
parent 4a40936293
commit d7b8b1864a
20 changed files with 2653 additions and 1923 deletions

View File

@@ -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
}