Almost done

This commit is contained in:
Daniel Arantes Loverde
2026-06-08 10:46:52 -03:00
parent aec1193944
commit 23d9752bfd
25 changed files with 476 additions and 257 deletions

View File

@@ -2,28 +2,39 @@ import Foundation
import SwiftUI
extension PizzaProductDetailSheet {
var stepSizes: some View {
let sizeItems = sizes
return VStack(alignment: .leading, spacing: 10) {
Text("1. Tamanho")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
ForEach(0..<sizeItems.count, id: \.self) { index in
sizeRow(sizeItems[index])
// MARK: - Steps
var stepSizes: some View {
accordionSection(
step: 0,
label: "Tamanho",
summary: selectedSize.map { "\($0.name ?? "") • Até \(max(1, $0.maxFlavors ?? 1)) sabor(es)" }
) {
ForEach(sizes) { size in
radioRow(
title: size.name ?? "Tamanho",
subtitle: "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)",
isSelected: selectedSizeId == size.id
) {
selectedSizeId = size.id
applyAutoSelections()
withAnimation(.easeInOut(duration: 0.2)) {
expandedStep = nextStep(after: 0)
}
}
}
}
.padding(12)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
var stepDoughs: some View {
VStack(alignment: .leading, spacing: 10) {
Text("2. Massa")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
accordionSection(
step: 1,
label: "Massa",
summary: doughs.count <= 1
? (doughs.first?.name ?? "Tradicional")
: doughs.first(where: { $0.id == selectedDoughId })?.name
) {
if doughs.count <= 1 {
Text(doughs.first?.name ?? "Massa tradicional")
.font(AppTypography.body)
@@ -36,21 +47,24 @@ extension PizzaProductDetailSheet {
isSelected: selectedDoughId == dough.id
) {
selectedDoughId = dough.id
applyAutoSelections()
withAnimation(.easeInOut(duration: 0.2)) {
expandedStep = nextStep(after: 1)
}
}
}
}
}
.padding(12)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
var stepCrusts: some View {
VStack(alignment: .leading, spacing: 10) {
Text("3. Borda")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
accordionSection(
step: 2,
label: "Borda",
summary: crusts.count <= 1
? crustDescription(crusts.first)
: crusts.first(where: { $0.id == selectedCrustId }).map { crustDescription($0) }
) {
if crusts.count <= 1 {
Text(crustDescription(crusts.first))
.font(AppTypography.body)
@@ -59,38 +73,40 @@ extension PizzaProductDetailSheet {
ForEach(crusts) { crust in
radioRow(
title: crust.name ?? "Borda",
subtitle: crust.priceModifier ?? 0 > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
subtitle: (crust.priceModifier ?? 0) > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
isSelected: selectedCrustId == crust.id
) {
selectedCrustId = crust.id
withAnimation(.easeInOut(duration: 0.2)) {
expandedStep = 3
}
}
}
}
}
.padding(12)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
var stepFlavors: some View {
let flavorItems = flavors
return VStack(alignment: .leading, spacing: 10) {
Text("4. Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
accordionSection(
step: 3,
label: "Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))",
summary: selectedFlavorIds.isEmpty ? nil
: selectedFlavorProducts.map(\.name).joined(separator: ", ")
) {
Text("Toque no sabor para escolher adicionais.")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
ForEach(0..<flavorItems.count, id: \.self) { index in
let flavor = flavorItems[index]
ForEach(flavors) { flavor in
let isSelected = selectedFlavorIds.contains(flavor.id)
let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price
let maxReached = selectedFlavorIds.count >= maxFlavorsAllowed
let disableSwitch = isSelected == false && maxReached
HStack(spacing: 10) {
AsyncStoreImage(imageURL: resolveImageURL(flavor.image))
.frame(width: 52, height: 52)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
VStack(alignment: .leading, spacing: 4) {
Text(flavor.name)
.font(AppTypography.body)
@@ -101,21 +117,15 @@ extension PizzaProductDetailSheet {
.foregroundStyle(AppColors.textMuted)
}
}
Spacer()
Toggle("", isOn: Binding(
get: { isSelected },
set: { value in
if value {
addFlavor(flavor.id)
} else {
removeFlavor(flavor.id)
}
if value { addFlavor(flavor.id) } else { removeFlavor(flavor.id) }
}
))
.labelsHidden()
.disabled(disableSwitch)
.disabled(!isSelected && maxReached)
}
.padding(.vertical, 2)
.appContentShape(Rectangle())
@@ -126,23 +136,90 @@ extension PizzaProductDetailSheet {
}
}
}
.padding(12)
}
// MARK: - Accordion container
func accordionSection(
step: Int,
label: String,
summary: String?,
@ViewBuilder content: () -> some View
) -> some View {
let isExpanded = expandedStep == step
let isDone = summary != nil
return VStack(spacing: 0) {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
expandedStep = isExpanded ? -1 : step
}
} label: {
HStack(spacing: 10) {
ZStack {
Circle()
.fill(isDone || isExpanded ? AppColors.primary : AppColors.textMuted.opacity(0.25))
.frame(width: 26, height: 26)
if isDone && !isExpanded {
Image(systemName: "checkmark")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(.white)
} else {
Text("\(step + 1)")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.white)
}
}
Text(label)
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
Spacer()
if let summary, !isExpanded {
Text(summary)
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: 140, alignment: .trailing)
}
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(AppColors.textMuted)
}
.padding(14)
}
.buttonStyle(.plain)
if isExpanded {
Divider().padding(.horizontal, 14)
VStack(alignment: .leading, spacing: 10) {
content()
}
.padding(14)
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
func sizeRow(_ size: StorePizzaSize) -> some View {
let title = size.name ?? "Tamanho"
let subtitle = "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)"
let selected = selectedSizeId == size.id
// MARK: - Helpers
return radioRow(
title: title,
subtitle: subtitle,
isSelected: selected
) {
selectedSizeId = size.id
func nextStep(after step: Int) -> Int {
if step == 0 {
if doughs.count > 1 { return 1 }
if crusts.count > 1 { return 2 }
return 3
}
if step == 1 {
if crusts.count > 1 { return 2 }
return 3
}
return 3
}
func radioRow(title: String, subtitle: String?, isSelected: Bool, action: @escaping () -> Void) -> some View {
@@ -158,7 +235,6 @@ extension PizzaProductDetailSheet {
.frame(width: 10, height: 10)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(AppTypography.body)
@@ -169,7 +245,6 @@ extension PizzaProductDetailSheet {
.foregroundStyle(AppColors.textMuted)
}
}
Spacer()
}
.appContentShape(Rectangle())
@@ -181,40 +256,29 @@ extension PizzaProductDetailSheet {
guard let crust else { return "Sem borda especial" }
let name = crust.name ?? "Borda"
let modifier = crust.priceModifier ?? 0
if modifier > 0 {
return "\(name) (+ \(formatCurrency(modifier)))"
}
return name
return modifier > 0 ? "\(name) (+ \(formatCurrency(modifier)))" : name
}
func applyAutoSelections() {
if selectedSizeId != nil {
if doughs.count == 1 {
selectedDoughId = doughs.first?.id
} else if doughs.isEmpty {
selectedDoughId = "__none__"
}
if doughs.count == 1 { selectedDoughId = doughs.first?.id }
else if doughs.isEmpty { selectedDoughId = "__none__" }
}
if isDoughReady {
if crusts.count == 1 {
selectedCrustId = crusts.first?.id
} else if crusts.isEmpty {
selectedCrustId = "__none__"
}
if crusts.count == 1 { selectedCrustId = crusts.first?.id }
else if crusts.isEmpty { selectedCrustId = "__none__" }
}
}
func trimFlavorSelectionByLimit() {
let limit = maxFlavorsAllowed
guard selectedFlavorIds.count > limit else { return }
let sorted = selectedFlavorIds.sorted()
selectedFlavorIds = Set(sorted.prefix(limit))
selectedFlavorIds = Set(selectedFlavorIds.sorted().prefix(limit))
}
func addFlavor(_ flavorId: String) {
if selectedFlavorIds.contains(flavorId) { return }
if selectedFlavorIds.count >= maxFlavorsAllowed { return }
guard !selectedFlavorIds.contains(flavorId),
selectedFlavorIds.count < maxFlavorsAllowed else { return }
selectedFlavorIds.insert(flavorId)
}