feat(home): restore planning tips and add map buttons to itinerary rows

- Create shared TipsSection component for displaying planning tips
- Add TipsSection to all 22 home content variants
- Fix displayedTips population with onAppear in HomeView
- Add map buttons to GameRowCompact (opens stadium in Apple Maps)
- Add map buttons to TravelRowView (opens driving directions)
- Add map buttons to CustomItemRowView (opens location when GPS available)
- Add AppleMapsLauncher.openLocation() and openDirections() methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-19 13:02:17 -06:00
parent 0e7fcb65fc
commit 1355c94236
26 changed files with 289 additions and 6 deletions

View File

@@ -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(

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -59,6 +59,12 @@ struct HomeContent_Airbnb: View {
exploreSection
}
// Planning tips
if !displayedTips.isEmpty {
TipsSection(tips: displayedTips)
.padding(.horizontal, 20)
}
Spacer(minLength: 40)
}
}

View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -69,6 +69,12 @@ struct HomeContent_CarrotWeather: View {
suggestionsSection
}
// Planning tips
if !displayedTips.isEmpty {
TipsSection(tips: displayedTips)
.padding(.horizontal, 20)
}
Spacer(minLength: 50)
}
}

View File

@@ -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)

View File

@@ -66,6 +66,12 @@ struct HomeContent_Fantastical: View {
suggestionsSection
}
// Planning tips
if !displayedTips.isEmpty {
TipsSection(tips: displayedTips)
.padding(.horizontal, 16)
}
Spacer(minLength: 40)
}
}

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -60,6 +60,12 @@ struct HomeContent_NikeRunClub: View {
challengesSection
}
// Planning tips
if !displayedTips.isEmpty {
TipsSection(tips: displayedTips)
.padding(.horizontal, 20)
}
Spacer(minLength: 50)
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -69,6 +69,12 @@ struct HomeContent_Strava: View {
routesSection
}
// Planning tips
if !displayedTips.isEmpty {
TipsSection(tips: displayedTips)
.padding(.horizontal, 16)
}
Spacer(minLength: 40)
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)