diff --git a/SportsTime/Core/Services/AppleMapsLauncher.swift b/SportsTime/Core/Services/AppleMapsLauncher.swift index 1075c4a..e948450 100644 --- a/SportsTime/Core/Services/AppleMapsLauncher.swift +++ b/SportsTime/Core/Services/AppleMapsLauncher.swift @@ -85,6 +85,42 @@ struct AppleMapsLauncher { open(chunks[index]) } + /// Opens a single location in Apple Maps + /// - Parameters: + /// - coordinate: Location to display + /// - name: Display name for the pin + static func openLocation(coordinate: CLLocationCoordinate2D, name: String) { + let placemark = MKPlacemark(coordinate: coordinate) + let mapItem = MKMapItem(placemark: placemark) + mapItem.name = name + mapItem.openInMaps(launchOptions: nil) + } + + /// Opens driving directions between two points in Apple Maps + /// - Parameters: + /// - from: Starting location + /// - fromName: Display name for origin + /// - to: Destination location + /// - toName: Display name for destination + static func openDirections( + from: CLLocationCoordinate2D, + fromName: String, + to: CLLocationCoordinate2D, + toName: String + ) { + let fromPlacemark = MKPlacemark(coordinate: from) + let fromItem = MKMapItem(placemark: fromPlacemark) + fromItem.name = fromName + + let toPlacemark = MKPlacemark(coordinate: to) + let toItem = MKMapItem(placemark: toPlacemark) + toItem.name = toName + + MKMapItem.openMaps(with: [fromItem, toItem], launchOptions: [ + MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving + ]) + } + // MARK: - Private Helpers private static func collectWaypoints( diff --git a/SportsTime/Features/Home/Views/HomeView.swift b/SportsTime/Features/Home/Views/HomeView.swift index e3ad8eb..01409dc 100644 --- a/SportsTime/Features/Home/Views/HomeView.swift +++ b/SportsTime/Features/Home/Views/HomeView.swift @@ -104,6 +104,11 @@ struct HomeView: View { await suggestedTripsGenerator.generateTrips() } } + .onAppear { + if displayedTips.isEmpty { + displayedTips = PlanningTips.random(3) + } + } .sheet(item: $selectedSuggestedTrip) { suggestedTrip in NavigationStack { TripDetailView(trip: suggestedTrip.trip) diff --git a/SportsTime/Features/Home/Views/TipsSection.swift b/SportsTime/Features/Home/Views/TipsSection.swift new file mode 100644 index 0000000..a4546ba --- /dev/null +++ b/SportsTime/Features/Home/Views/TipsSection.swift @@ -0,0 +1,43 @@ +// +// TipsSection.swift +// SportsTime +// +// Shared component for displaying planning tips across all home content variants. +// Uses Theme colors to adapt to light/dark mode and various design styles. +// + +import SwiftUI + +struct TipsSection: View { + let tips: [PlanningTip] + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + if !tips.isEmpty { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Planning Tips") + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + VStack(spacing: Theme.Spacing.xs) { + ForEach(tips) { tip in + TipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle) + } + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + } + } +} + +#Preview { + let tips = PlanningTips.random(3) + VStack { + TipsSection(tips: tips) + .padding() + } + .background(Color.gray.opacity(0.1)) +} diff --git a/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift b/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift index 9ae7f88..4eb520c 100644 --- a/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift +++ b/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift @@ -59,6 +59,12 @@ struct HomeContent_Airbnb: View { exploreSection } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + Spacer(minLength: 40) } } diff --git a/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift b/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift index bc1f273..2149099 100644 --- a/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift +++ b/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift @@ -65,7 +65,14 @@ struct HomeContent_AppleMaps: View { // Explore section exploreSection .padding(.horizontal, 16) - .padding(.bottom, 32) + + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + + Spacer().frame(height: 32) } } .background(bgColor.ignoresSafeArea()) diff --git a/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift b/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift index a9d56e5..ec8a1c7 100644 --- a/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift +++ b/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift @@ -68,6 +68,13 @@ struct HomeContent_ArtDeco: View { .padding(.horizontal, 24) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.top, 48) + .padding(.horizontal, 24) + } + // DECO FOOTER decoFooter .padding(.top, 56) diff --git a/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift b/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift index 55753d5..1ee8811 100644 --- a/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift +++ b/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift @@ -55,6 +55,14 @@ struct HomeContent_Brutalist: View { if !savedTrips.isEmpty { savedSection .padding(.vertical, 24) + + perforatedDivider + } + + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.vertical, 24) } // FOOTER STAMP diff --git a/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift b/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift index 2a1b809..5fd02e9 100644 --- a/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift +++ b/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift @@ -69,6 +69,12 @@ struct HomeContent_CarrotWeather: View { suggestionsSection } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + Spacer(minLength: 50) } } diff --git a/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift b/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift index d35efd2..319475e 100644 --- a/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift +++ b/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift @@ -69,6 +69,13 @@ struct HomeContent_DarkIndustrial: View { .padding(.horizontal, 20) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.top, 40) + .padding(.horizontal, 20) + } + // INDUSTRIAL FOOTER industrialFooter .padding(.top, 48) diff --git a/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift b/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift index d3b8d79..19dd5f0 100644 --- a/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift +++ b/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift @@ -66,6 +66,12 @@ struct HomeContent_Fantastical: View { suggestionsSection } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 40) } } diff --git a/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift b/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift index 511924d..9608cac 100644 --- a/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift +++ b/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift @@ -72,7 +72,14 @@ struct HomeContent_Flighty: View { // Stats dashboard statsDashboard .padding(.horizontal, 20) - .padding(.bottom, 32) + + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + + Spacer().frame(height: 32) } } .background(bgColor.ignoresSafeArea()) diff --git a/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift b/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift index 18a0219..82aebf5 100644 --- a/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift +++ b/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift @@ -48,6 +48,12 @@ struct HomeContent_Glassmorphism: View { .padding(.horizontal, 16) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 32) } } diff --git a/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift b/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift index 99e2c38..697422a 100644 --- a/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift +++ b/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift @@ -65,6 +65,13 @@ struct HomeContent_LuxuryEditorial: View { .padding(.bottom, 48) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + // COLOPHON colophon .padding(.bottom, 32) diff --git a/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift b/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift index 170353e..ec3d650 100644 --- a/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift +++ b/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift @@ -54,6 +54,12 @@ struct HomeContent_MaximalistChaos: View { .padding(.horizontal, 16) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 60) } } diff --git a/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift b/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift index 8a85cd0..482da8f 100644 --- a/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift +++ b/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift @@ -57,6 +57,12 @@ struct HomeContent_NeoBrutalist: View { .padding(.horizontal, 16) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 32) } } diff --git a/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift b/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift index b484cfc..d02a1a0 100644 --- a/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift +++ b/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift @@ -60,6 +60,12 @@ struct HomeContent_NikeRunClub: View { challengesSection } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + Spacer(minLength: 50) } } diff --git a/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift b/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift index 101eed7..34f868a 100644 --- a/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift +++ b/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift @@ -59,6 +59,12 @@ struct HomeContent_Organic: View { .padding(.horizontal, 20) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + // FOOTER LEAF footerLeaf .padding(.bottom, 32) diff --git a/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift b/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift index 9748691..9b3c15f 100644 --- a/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift +++ b/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift @@ -59,6 +59,12 @@ struct HomeContent_Playful: View { .padding(.horizontal, 20) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + // FUN FOOTER funFooter .padding(.bottom, 32) diff --git a/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift b/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift index 89ebdb7..49077ba 100644 --- a/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift +++ b/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift @@ -56,6 +56,12 @@ struct HomeContent_RetroFuturism: View { .padding(.horizontal, 16) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + // RETRO FOOTER retroFooter .padding(.bottom, 32) diff --git a/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift b/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift index 2cd4167..a2d4061 100644 --- a/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift +++ b/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift @@ -65,7 +65,13 @@ struct HomeContent_SeatGeek: View { // Browse by sport browseBySportSection .padding(.horizontal, 16) - .padding(.bottom, 32) + + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + .padding(.bottom, 32) + } } } .background(bgColor.ignoresSafeArea()) diff --git a/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift b/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift index 1fd5c92..9bb37f8 100644 --- a/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift +++ b/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift @@ -67,6 +67,12 @@ struct HomeContent_SoftPastel: View { .padding(.horizontal, 20) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 20) + } + // SOFT FOOTER softFooter .padding(.bottom, 32) diff --git a/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift b/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift index d7364b9..6faa95b 100644 --- a/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift +++ b/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift @@ -49,6 +49,12 @@ struct HomeContent_Spotify: View { // Browse sports browseSportsSection + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 50) } } diff --git a/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift b/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift index 72a0374..60593c7 100644 --- a/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift +++ b/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift @@ -69,6 +69,12 @@ struct HomeContent_Strava: View { routesSection } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 16) + } + Spacer(minLength: 40) } } diff --git a/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift b/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift index 57d866d..90e2213 100644 --- a/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift +++ b/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift @@ -72,6 +72,13 @@ struct HomeContent_SwissModernist: View { .padding(.horizontal, 24) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.top, 64) + .padding(.horizontal, 24) + } + // FOOTER swissFooter .padding(.top, 80) diff --git a/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift b/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift index 9d77aa4..60e03e8 100644 --- a/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift +++ b/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift @@ -73,6 +73,13 @@ struct HomeContent_Things3: View { .padding(.top, savedTrips.isEmpty ? 0 : 28) } + // Planning tips + if !displayedTips.isEmpty { + TipsSection(tips: displayedTips) + .padding(.horizontal, 24) + .padding(.top, 28) + } + Spacer(minLength: 60) } } diff --git a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift index 54ac873..02eaa9c 100644 --- a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift +++ b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift @@ -1154,12 +1154,29 @@ struct GameRowCompact: View { } Spacer() - + // Game time (prominently displayed) Text(formattedTime) .font(.title3) .fontWeight(.medium) .foregroundStyle(Theme.warmOrange) + + // Map button to open stadium location + Button { + AppleMapsLauncher.openLocation( + coordinate: richGame.stadium.coordinate, + name: richGame.stadium.name + ) + } label: { + Image(systemName: "map") + .font(.body) + .foregroundStyle(Theme.warmOrange) + .padding(8) + .background(Theme.warmOrange.opacity(0.15)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel("Open \(richGame.stadium.name) in Maps") } .padding(Theme.Spacing.md) .background(Theme.cardBackgroundElevated(colorScheme)) @@ -1217,8 +1234,30 @@ struct TravelRowView: View { .fontWeight(.medium) .foregroundStyle(Theme.textPrimary(colorScheme)) } - + Spacer() + + // Map button to open driving directions + if let fromCoord = segment.fromLocation.coordinate, + let toCoord = segment.toLocation.coordinate { + Button { + AppleMapsLauncher.openDirections( + from: fromCoord, + fromName: segment.fromLocation.name, + to: toCoord, + toName: segment.toLocation.name + ) + } label: { + Image(systemName: "map") + .font(.body) + .foregroundStyle(Theme.warmOrange) + .padding(8) + .background(Theme.warmOrange.opacity(0.15)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel("Get directions from \(segment.fromLocation.name) to \(segment.toLocation.name)") + } } .padding(Theme.Spacing.md) .background(Theme.cardBackground(colorScheme)) @@ -1281,7 +1320,26 @@ struct CustomItemRowView: View { } Spacer() - + + // Map button for items with GPS coordinates + if let info = customInfo, let coordinate = info.coordinate { + Button { + AppleMapsLauncher.openLocation( + coordinate: coordinate, + name: info.title + ) + } label: { + Image(systemName: "map") + .font(.subheadline) + .foregroundStyle(Theme.warmOrange) + .padding(6) + .background(Theme.warmOrange.opacity(0.15)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel("Open \(info.title) in Maps") + } + // Chevron indicates this is tappable Image(systemName: "chevron.right") .foregroundStyle(.tertiary)