Almost done
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user