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,196 @@
import Foundation
import SwiftUI
struct ProductDetailSheet: View {
let product: StoreCatalogProduct
let imageURL: String?
let storeId: String
let currentQuantityForItemId: (String) -> Int
let onAdd: (CartItemState) -> Void
@Environment(\.dismiss) var dismiss
@State var selectedAddonItemIds: Set<String> = []
@State var quantity: Int = 0
private var selectedAddonItems: [StoreAddonItem] {
product.addonGroups
.flatMap(\.items)
.filter { selectedAddonItemIds.contains($0.id) }
}
private var addonsTotal: Double {
selectedAddonItems.reduce(0) { $0 + ($1.price ?? 0) }
}
private var unitPrice: Double {
(product.price ?? 0) + addonsTotal
}
private var totalPrice: Double {
unitPrice * Double(quantity)
}
private var cartItemId: String {
let addonKey = selectedAddonItemIds.sorted().joined(separator: ",")
return "\(storeId)::\(product.id)::\(addonKey)"
}
private var selectedAddonsSummary: String? {
let names = selectedAddonItems.map(\.name)
if names.isEmpty { return nil }
return names.joined(separator: ", ")
}
private var addButtonTitle: String {
if quantity <= 0 {
return "Remover do carrinho"
}
return "Atualizar • \(formatCurrency(totalPrice))"
}
var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 16) {
AsyncStoreImage(imageURL: imageURL)
.frame(height: 220)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
Text(product.name)
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
if let description = product.description, description.isEmpty == false {
Text(description)
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
}
Text(formatCurrency(unitPrice))
.font(AppTypography.heading2)
.foregroundStyle(AppColors.primary)
if addonsTotal > 0 {
Text("Inclui adicionais: \(formatCurrency(addonsTotal))")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
}
if product.addonGroups.isEmpty == false {
Text("Adicionais")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
ForEach(product.addonGroups) { group in
VStack(alignment: .leading, spacing: 8) {
Text(group.name)
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
ForEach(group.items) { item in
Button {
if selectedAddonItemIds.contains(item.id) {
selectedAddonItemIds.remove(item.id)
} else {
selectedAddonItemIds.insert(item.id)
}
} label: {
HStack {
Image(systemName: selectedAddonItemIds.contains(item.id) ? "checkmark.circle.fill" : "circle")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(selectedAddonItemIds.contains(item.id) ? AppColors.primary : AppColors.textMuted)
Text(item.name)
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
Spacer()
Text(String(format: "+ R$ %.2f", item.price ?? 0).replacingOccurrences(of: ".", with: ","))
.font(AppTypography.body)
.foregroundStyle(AppColors.textPrimary)
}
}
.buttonStyle(.plain)
}
}
.padding(12)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
}
}
.padding(20)
.padding(.bottom, 80)
}
.safeAreaInset(edge: .bottom) {
HStack(spacing: 12) {
HStack(spacing: 10) {
Button(action: { if quantity > 0 { quantity -= 1 } }) {
Image(systemName: "minus")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AppColors.textPrimary)
.frame(width: 32, height: 32)
.background(AppColors.surface)
.clipShape(Circle())
}
.buttonStyle(.plain)
.disabled(quantity <= 0)
Text("\(quantity)")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
.frame(minWidth: 20)
Button(action: { quantity += 1 }) {
Image(systemName: "plus")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AppColors.textPrimary)
.frame(width: 32, height: 32)
.background(AppColors.surface)
.clipShape(Circle())
}
.buttonStyle(.plain)
}
.padding(.horizontal, 8)
.frame(height: 48)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.buttonStyle(.plain)
PrimaryButton(title: addButtonTitle) {
let item = CartItemState(
id: cartItemId,
productId: product.id,
storeId: storeId,
name: product.name,
details: selectedAddonsSummary,
quantity: quantity,
unitPrice: unitPrice
)
onAdd(item)
dismiss()
}
}
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 12)
.background(.ultraThinMaterial)
}
.background(AppColors.backgroundLight.ignoresSafeArea())
.navigationTitle("Detalhes")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Fechar") { dismiss() }
.foregroundStyle(AppColors.primary)
}
}
.onAppear {
quantity = currentQuantityForItemId(cartItemId)
}
.onChange(of: selectedAddonItemIds) { _, _ in
quantity = currentQuantityForItemId(cartItemId)
}
}
private func formatCurrency(_ value: Double) -> String {
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
}
}