feat(ui): add sport backgrounds to share cards, achievement filtering, and wizard validation

- Add ShareCardSportBackground with floating sport icons for share cards
- Share cards now show sport-specific backgrounds (single or multiple sports)
- Achievement collection share respects sport filter selection
- Add ability to share individual achievements from detail sheet
- Trip wizard ReviewStep highlights missing required fields in red
- Add FieldValidation model to TripWizardViewModel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-14 12:02:57 -06:00
parent 1e26cfebc8
commit 3d4952e5ff
9 changed files with 255 additions and 23 deletions

View File

@@ -77,6 +77,17 @@ final class TripWizardViewModel {
hasSetRepeatCities
}
/// Field validation for the review step - shows which fields are missing
var fieldValidation: FieldValidation {
FieldValidation(
sports: selectedSports.isEmpty ? .missing : .valid,
dates: hasSetDates ? .valid : .missing,
regions: selectedRegions.isEmpty ? .missing : .valid,
routePreference: hasSetRoutePreference ? .valid : .missing,
repeatCities: hasSetRepeatCities ? .valid : .missing
)
}
// MARK: - Sport Availability
func canSelectSport(_ sport: Sport) -> Bool {
@@ -120,3 +131,18 @@ final class TripWizardViewModel {
mustStopLocations = []
}
}
// MARK: - Field Validation
struct FieldValidation {
enum Status {
case valid
case missing
}
let sports: Status
let dates: Status
let regions: Status
let routePreference: Status
let repeatCities: Status
}

View File

@@ -19,6 +19,7 @@ struct ReviewStep: View {
let mustStopLocations: [LocationInput]
let isPlanning: Bool
let canPlanTrip: Bool
let fieldValidation: FieldValidation
let onPlan: () -> Void
var body: some View {
@@ -30,11 +31,31 @@ struct ReviewStep: View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
ReviewRow(label: "Mode", value: planningMode.displayName)
ReviewRow(label: "Sports", value: selectedSports.map(\.rawValue).sorted().joined(separator: ", "))
ReviewRow(label: "Dates", value: dateRangeText)
ReviewRow(label: "Regions", value: selectedRegions.map(\.shortName).sorted().joined(separator: ", "))
ReviewRow(label: "Route", value: routePreference.displayName)
ReviewRow(label: "Repeat cities", value: allowRepeatCities ? "Yes" : "No")
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
)
if !mustStopLocations.isEmpty {
ReviewRow(label: "Must-stops", value: mustStopLocations.map(\.name).joined(separator: ", "))
@@ -83,26 +104,33 @@ private struct ReviewRow: View {
@Environment(\.colorScheme) private var colorScheme
let label: String
let value: String
var isMissing: Bool = false
var body: some View {
HStack(alignment: .top) {
Text(label)
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.foregroundStyle(isMissing ? .red : Theme.textMuted(colorScheme))
.frame(width: 80, alignment: .leading)
Text(value)
.font(.subheadline)
.foregroundStyle(Theme.textPrimary(colorScheme))
.foregroundStyle(isMissing ? .red : Theme.textPrimary(colorScheme))
Spacer()
if isMissing {
Image(systemName: "exclamationmark.circle.fill")
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
// MARK: - Preview
#Preview {
#Preview("All Valid") {
ReviewStep(
planningMode: .dateRange,
selectedSports: [.mlb, .nba],
@@ -114,6 +142,38 @@ private struct ReviewRow: View {
mustStopLocations: [],
isPlanning: false,
canPlanTrip: true,
fieldValidation: FieldValidation(
sports: .valid,
dates: .valid,
regions: .valid,
routePreference: .valid,
repeatCities: .valid
),
onPlan: {}
)
.padding()
.themedBackground()
}
#Preview("Missing Fields") {
ReviewStep(
planningMode: .dateRange,
selectedSports: [],
startDate: Date(),
endDate: Date().addingTimeInterval(86400 * 7),
selectedRegions: [],
routePreference: .balanced,
allowRepeatCities: false,
mustStopLocations: [],
isPlanning: false,
canPlanTrip: false,
fieldValidation: FieldValidation(
sports: .missing,
dates: .valid,
regions: .missing,
routePreference: .valid,
repeatCities: .missing
),
onPlan: {}
)
.padding()

View File

@@ -72,6 +72,7 @@ struct TripWizardView: View {
mustStopLocations: viewModel.mustStopLocations,
isPlanning: viewModel.isPlanning,
canPlanTrip: viewModel.canPlanTrip,
fieldValidation: viewModel.fieldValidation,
onPlan: { Task { await planTrip() } }
)
}