diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/DatesStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/DatesStep.swift new file mode 100644 index 0000000..9a80ebb --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/DatesStep.swift @@ -0,0 +1,98 @@ +// +// DatesStep.swift +// SportsTime +// +// Step 3 of the trip wizard - select travel dates. +// + +import SwiftUI + +struct DatesStep: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var startDate: Date + @Binding var endDate: Date + @Binding var hasSetDates: Bool + let onDatesChanged: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "When would you like to travel?", + subtitle: "Pick your trip dates" + ) + + VStack(spacing: Theme.Spacing.md) { + DatePicker( + "Start Date", + selection: $startDate, + in: Date()..., + displayedComponents: .date + ) + .datePickerStyle(.compact) + .onChange(of: startDate) { _, newValue in + // Ensure end date is after start date + if endDate < newValue { + endDate = newValue.addingTimeInterval(86400) + } + hasSetDates = true + onDatesChanged() + } + + DatePicker( + "End Date", + selection: $endDate, + in: startDate..., + displayedComponents: .date + ) + .datePickerStyle(.compact) + .onChange(of: endDate) { _, _ in + hasSetDates = true + onDatesChanged() + } + } + .padding(Theme.Spacing.sm) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + + // Trip duration indicator + HStack { + Image(systemName: "calendar.badge.clock") + .foregroundStyle(Theme.textMuted(colorScheme)) + Text(durationText) + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + + private var durationText: String { + let days = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day ?? 0 + if days == 0 { + return "Same day trip" + } else if days == 1 { + return "1 day trip" + } else { + return "\(days) day trip" + } + } +} + +// MARK: - Preview + +#Preview { + DatesStep( + startDate: .constant(Date()), + endDate: .constant(Date().addingTimeInterval(86400 * 5)), + hasSetDates: .constant(true), + onDatesChanged: {} + ) + .padding() + .themedBackground() +} diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/MustStopsStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/MustStopsStep.swift new file mode 100644 index 0000000..1c0cab4 --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/MustStopsStep.swift @@ -0,0 +1,85 @@ +// +// MustStopsStep.swift +// SportsTime +// +// Step 7 of the trip wizard - add must-stop locations. +// + +import SwiftUI + +struct MustStopsStep: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var mustStopLocations: [LocationInput] + @State private var showLocationSearch = false + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "Any must-stop locations?", + subtitle: "Optional - add cities you want to visit" + ) + + if !mustStopLocations.isEmpty { + VStack(spacing: Theme.Spacing.xs) { + ForEach(mustStopLocations, id: \.name) { location in + HStack { + Image(systemName: "mappin.circle.fill") + .foregroundStyle(Theme.warmOrange) + + Text(location.name) + .font(.subheadline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + + Button { + mustStopLocations.removeAll { $0.name == location.name } + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(Theme.textMuted(colorScheme)) + } + } + .padding(Theme.Spacing.sm) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + } + } + + Button { + showLocationSearch = true + } label: { + HStack { + Image(systemName: "plus.circle.fill") + Text(mustStopLocations.isEmpty ? "Add a location" : "Add another") + } + .font(.subheadline) + .foregroundStyle(Theme.warmOrange) + } + + Text("Skip this step if you don't have specific cities in mind") + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + .sheet(isPresented: $showLocationSearch) { + LocationSearchSheet(inputType: .mustStop) { location in + mustStopLocations.append(location) + } + } + } +} + +// MARK: - Preview + +#Preview { + MustStopsStep(mustStopLocations: .constant([])) + .padding() + .themedBackground() +} diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/RegionsStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/RegionsStep.swift new file mode 100644 index 0000000..315161a --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/RegionsStep.swift @@ -0,0 +1,104 @@ +// +// RegionsStep.swift +// SportsTime +// +// Step 4 of the trip wizard - select geographic regions. +// + +import SwiftUI + +struct RegionsStep: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var selectedRegions: Set + + private let columns = [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "Where do you want to go?", + subtitle: "Select one or more regions" + ) + + LazyVGrid(columns: columns, spacing: Theme.Spacing.sm) { + ForEach(Region.allCases.filter { $0 != .crossCountry }) { region in + RegionCard( + region: region, + isSelected: selectedRegions.contains(region), + onTap: { toggleRegion(region) } + ) + } + } + + if !selectedRegions.isEmpty { + HStack { + Image(systemName: "mappin.circle.fill") + .foregroundStyle(Theme.warmOrange) + Text("\(selectedRegions.count) region\(selectedRegions.count == 1 ? "" : "s") selected") + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + } + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + + private func toggleRegion(_ region: Region) { + if selectedRegions.contains(region) { + selectedRegions.remove(region) + } else { + selectedRegions.insert(region) + } + } +} + +// MARK: - Region Card + +private struct RegionCard: View { + @Environment(\.colorScheme) private var colorScheme + let region: Region + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + VStack(spacing: Theme.Spacing.xs) { + Image(systemName: region.iconName) + .font(.title) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme)) + + Text(region.shortName) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textPrimary(colorScheme)) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, Theme.Spacing.md) + .background(isSelected ? Theme.warmOrange.opacity(0.1) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - Preview + +#Preview { + RegionsStep(selectedRegions: .constant([.east, .central])) + .padding() + .themedBackground() +} diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/RepeatCitiesStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/RepeatCitiesStep.swift new file mode 100644 index 0000000..4e0df5b --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/RepeatCitiesStep.swift @@ -0,0 +1,98 @@ +// +// RepeatCitiesStep.swift +// SportsTime +// +// Step 6 of the trip wizard - allow repeat city visits. +// + +import SwiftUI + +struct RepeatCitiesStep: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var allowRepeatCities: Bool + @Binding var hasSetRepeatCities: Bool + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "Visit cities more than once?", + subtitle: "Some trips work better with return visits" + ) + + HStack(spacing: Theme.Spacing.md) { + OptionButton( + title: "No, unique cities only", + icon: "arrow.right", + isSelected: hasSetRepeatCities && !allowRepeatCities, + onTap: { + allowRepeatCities = false + hasSetRepeatCities = true + } + ) + + OptionButton( + title: "Yes, allow repeats", + icon: "arrow.triangle.2.circlepath", + isSelected: hasSetRepeatCities && allowRepeatCities, + onTap: { + allowRepeatCities = true + hasSetRepeatCities = true + } + ) + } + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } +} + +// MARK: - Option Button + +private struct OptionButton: View { + @Environment(\.colorScheme) private var colorScheme + let title: String + let icon: String + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + VStack(spacing: Theme.Spacing.sm) { + Image(systemName: icon) + .font(.title2) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme)) + + Text(title) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textPrimary(colorScheme)) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(Theme.Spacing.md) + .background(isSelected ? Theme.warmOrange.opacity(0.1) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - Preview + +#Preview { + RepeatCitiesStep( + allowRepeatCities: .constant(false), + hasSetRepeatCities: .constant(true) + ) + .padding() + .themedBackground() +} diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift new file mode 100644 index 0000000..b42cb6a --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift @@ -0,0 +1,120 @@ +// +// ReviewStep.swift +// SportsTime +// +// Step 8 of the trip wizard - review and submit. +// + +import SwiftUI + +struct ReviewStep: View { + @Environment(\.colorScheme) private var colorScheme + let planningMode: PlanningMode + let selectedSports: Set + let startDate: Date + let endDate: Date + let selectedRegions: Set + let routePreference: RoutePreference + let allowRepeatCities: Bool + let mustStopLocations: [LocationInput] + let isPlanning: Bool + let onPlan: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "Ready to plan your trip!", + subtitle: "Review your selections" + ) + + 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") + + if !mustStopLocations.isEmpty { + ReviewRow(label: "Must-stops", value: mustStopLocations.map(\.name).joined(separator: ", ")) + } + } + .padding(Theme.Spacing.sm) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + + Button(action: onPlan) { + HStack { + if isPlanning { + ProgressView() + .scaleEffect(0.8) + .tint(.white) + } + Text(isPlanning ? "Planning..." : "Plan My Trip") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding(Theme.Spacing.md) + .background(Theme.warmOrange) + .foregroundStyle(.white) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + } + .disabled(isPlanning) + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + + private var dateRangeText: String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))" + } +} + +// MARK: - Review Row + +private struct ReviewRow: View { + @Environment(\.colorScheme) private var colorScheme + let label: String + let value: String + + var body: some View { + HStack(alignment: .top) { + Text(label) + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + .frame(width: 80, alignment: .leading) + + Text(value) + .font(.subheadline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + } + } +} + +// MARK: - Preview + +#Preview { + ReviewStep( + planningMode: .dateRange, + selectedSports: [.mlb, .nba], + startDate: Date(), + endDate: Date().addingTimeInterval(86400 * 7), + selectedRegions: [.east, .central], + routePreference: .balanced, + allowRepeatCities: false, + mustStopLocations: [], + isPlanning: false, + onPlan: {} + ) + .padding() + .themedBackground() +} diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/RoutePreferenceStep.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/RoutePreferenceStep.swift new file mode 100644 index 0000000..dc985c0 --- /dev/null +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/RoutePreferenceStep.swift @@ -0,0 +1,119 @@ +// +// RoutePreferenceStep.swift +// SportsTime +// +// Step 5 of the trip wizard - select route preference. +// + +import SwiftUI + +struct RoutePreferenceStep: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var routePreference: RoutePreference + @Binding var hasSetRoutePreference: Bool + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + StepHeader( + title: "What's your route preference?", + subtitle: "Balance efficiency vs. exploration" + ) + + VStack(spacing: Theme.Spacing.sm) { + ForEach(RoutePreference.allCases) { preference in + RoutePreferenceCard( + preference: preference, + isSelected: routePreference == preference, + onTap: { + routePreference = preference + hasSetRoutePreference = true + } + ) + } + } + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } +} + +// MARK: - Route Preference Card + +private struct RoutePreferenceCard: View { + @Environment(\.colorScheme) private var colorScheme + let preference: RoutePreference + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: Theme.Spacing.md) { + Image(systemName: preference.iconName) + .font(.title2) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme)) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text(preference.displayName) + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Text(preference.descriptionText) + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + + Spacer() + + if isSelected { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(Theme.warmOrange) + } + } + .padding(Theme.Spacing.md) + .background(isSelected ? Theme.warmOrange.opacity(0.1) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - RoutePreference Extensions + +extension RoutePreference { + var iconName: String { + switch self { + case .direct: return "bolt.fill" + case .scenic: return "binoculars.fill" + case .balanced: return "scale.3d" + } + } + + var descriptionText: String { + switch self { + case .direct: return "Minimize driving time between games" + case .scenic: return "Prioritize interesting stops and routes" + case .balanced: return "Mix of efficiency and exploration" + } + } +} + +// MARK: - Preview + +#Preview { + RoutePreferenceStep( + routePreference: .constant(.balanced), + hasSetRoutePreference: .constant(true) + ) + .padding() + .themedBackground() +}