feat(wizard): add mode-specific trip wizard inputs
- Add GamePickerStep with sheet-based Sport → Team → Game selection - Add TeamPickerStep with sheet-based Sport → Team selection - Add LocationsStep for start/end location selection with round trip toggle - Update TripWizardViewModel with mode-specific fields and validation - Update TripWizardView with conditional step rendering per mode - Update ReviewStep with mode-aware validation display - Fix gameFirst mode to derive date range from selected games Each planning mode now shows only relevant steps: - By Dates: Dates → Sports → Regions → Route → Repeat → Must Stops - By Games: Game Picker → Route → Repeat → Must Stops - By Route: Locations → Dates → Sports → Route → Repeat → Must Stops - Follow Team: Team Picker → Dates → Route → Repeat → Must Stops Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,12 @@ struct ReviewStep: View {
|
||||
let fieldValidation: FieldValidation
|
||||
let onPlan: () -> Void
|
||||
|
||||
// Mode-specific display values (passed from parent)
|
||||
var selectedGameCount: Int = 0
|
||||
var selectedTeamName: String? = nil
|
||||
var startLocationName: String? = nil
|
||||
var endLocationName: String? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
|
||||
StepHeader(
|
||||
@@ -30,33 +36,19 @@ struct ReviewStep: View {
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
// Mode (always shown)
|
||||
ReviewRow(label: "Mode", value: planningMode.displayName)
|
||||
ReviewRow(
|
||||
label: "Sports",
|
||||
value: selectedSports.isEmpty ? "Not selected" : selectedSports.map(\.rawValue).sorted().joined(separator: ", "),
|
||||
isMissing: fieldValidation.sports == .missing
|
||||
)
|
||||
ReviewRow(
|
||||
label: "Dates",
|
||||
value: dateRangeText,
|
||||
isMissing: fieldValidation.dates == .missing
|
||||
)
|
||||
ReviewRow(
|
||||
label: "Regions",
|
||||
value: selectedRegions.isEmpty ? "Not selected" : selectedRegions.map(\.shortName).sorted().joined(separator: ", "),
|
||||
isMissing: fieldValidation.regions == .missing
|
||||
)
|
||||
ReviewRow(
|
||||
label: "Route",
|
||||
value: routePreference.displayName,
|
||||
isMissing: fieldValidation.routePreference == .missing
|
||||
)
|
||||
ReviewRow(
|
||||
label: "Repeat cities",
|
||||
value: allowRepeatCities ? "Yes" : "No",
|
||||
isMissing: fieldValidation.repeatCities == .missing
|
||||
)
|
||||
|
||||
// Mode-specific required fields
|
||||
ForEach(fieldValidation.requiredFields, id: \.name) { field in
|
||||
ReviewRow(
|
||||
label: field.name,
|
||||
value: displayValue(for: field.name),
|
||||
isMissing: field.status == .missing
|
||||
)
|
||||
}
|
||||
|
||||
// Optional: Must-stops (shown if any selected)
|
||||
if !mustStopLocations.isEmpty {
|
||||
ReviewRow(label: "Must-stops", value: mustStopLocations.map(\.name).joined(separator: ", "))
|
||||
}
|
||||
@@ -65,6 +57,17 @@ struct ReviewStep: View {
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
|
||||
// Missing fields warning
|
||||
if !fieldValidation.missingFields.isEmpty {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
Text("Complete all required fields to continue")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: onPlan) {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
if isPlanning {
|
||||
@@ -91,6 +94,31 @@ struct ReviewStep: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func displayValue(for fieldName: String) -> String {
|
||||
switch fieldName {
|
||||
case "Sports":
|
||||
return selectedSports.isEmpty ? "Not selected" : selectedSports.map(\.rawValue).sorted().joined(separator: ", ")
|
||||
case "Dates":
|
||||
return dateRangeText
|
||||
case "Regions":
|
||||
return selectedRegions.isEmpty ? "Not selected" : selectedRegions.map(\.shortName).sorted().joined(separator: ", ")
|
||||
case "Route Preference":
|
||||
return routePreference.displayName
|
||||
case "Repeat Cities":
|
||||
return allowRepeatCities ? "Yes" : "No"
|
||||
case "Games":
|
||||
return selectedGameCount > 0 ? "\(selectedGameCount) game\(selectedGameCount == 1 ? "" : "s") selected" : "Not selected"
|
||||
case "Team":
|
||||
return selectedTeamName ?? "Not selected"
|
||||
case "Start Location":
|
||||
return startLocationName ?? "Not selected"
|
||||
case "End Location":
|
||||
return endLocationName ?? "Not selected"
|
||||
default:
|
||||
return "—"
|
||||
}
|
||||
}
|
||||
|
||||
private var dateRangeText: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
@@ -143,11 +171,16 @@ private struct ReviewRow: View {
|
||||
isPlanning: false,
|
||||
canPlanTrip: true,
|
||||
fieldValidation: FieldValidation(
|
||||
planningMode: .dateRange,
|
||||
sports: .valid,
|
||||
dates: .valid,
|
||||
regions: .valid,
|
||||
routePreference: .valid,
|
||||
repeatCities: .valid
|
||||
repeatCities: .valid,
|
||||
selectedGames: .valid,
|
||||
selectedTeam: .valid,
|
||||
startLocation: .valid,
|
||||
endLocation: .valid
|
||||
),
|
||||
onPlan: {}
|
||||
)
|
||||
@@ -168,11 +201,16 @@ private struct ReviewRow: View {
|
||||
isPlanning: false,
|
||||
canPlanTrip: false,
|
||||
fieldValidation: FieldValidation(
|
||||
planningMode: .dateRange,
|
||||
sports: .missing,
|
||||
dates: .valid,
|
||||
regions: .missing,
|
||||
routePreference: .valid,
|
||||
repeatCities: .missing
|
||||
repeatCities: .missing,
|
||||
selectedGames: .valid,
|
||||
selectedTeam: .valid,
|
||||
startLocation: .valid,
|
||||
endLocation: .valid
|
||||
),
|
||||
onPlan: {}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user