Files
Sportstime/docs/plans/2026-01-12-polish-enhancements-design.md
Trey t 3530b31cca docs: add 3 feature enhancement design plans
- Trip Planning Enhancements: progressive reveal single-screen wizard
- Progress Tracking Enhancements: multiple visits, games history, zoomable map
- Polish Enhancements: grouped sorting, 100+ planning tips

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 19:07:46 -06:00

6.8 KiB

Polish Enhancements Design

Date: 2026-01-12 Status: Draft Scope: High-level overview for scoping/prioritization

Goal

Quick-win improvements for trip browsing and home screen engagement.

Features

  1. Grouped sorting in trip options - When sorting by "Most Cities", group results into sections (12 cities, 11 cities...)
  2. 100+ planning tips - Expand from 3 hardcoded tips to 100+, randomly show 3

Current State

  • TripOptionsView has sort picker (Most Cities, Most Games, etc.) but results are a flat list
  • HomeView.tipsSection shows 3 hardcoded TipRow items

Feature Details

1. Grouped Sorting in Trip Options

When user selects a sort option (except "Recommended"), group results into sections:

Sort Option Section Headers Example
Most Cities By count descending "12 cities", "11 cities", "10 cities"...
Most Games By count descending "8 games", "7 games", "6 games"...
Least Games By count ascending "1 game", "2 games", "3 games"...
Most Miles By range descending "2000+ mi", "1500-2000 mi", "1000-1500 mi"...
Least Miles By range ascending "0-500 mi", "500-1000 mi"...
Best Efficiency No grouping Continuous metric, flat list
Recommended No grouping Default sort, flat list

Implementation:

// In TripOptionsView
private var groupedOptions: [(header: String, options: [ItineraryOption])] {
    switch sortOption {
    case .recommended, .bestEfficiency:
        return [("", filteredAndSortedOptions)] // Flat list, no header

    case .mostCities:
        return Dictionary(grouping: filteredAndSortedOptions) { uniqueCityCount(for: $0) }
            .sorted { $0.key > $1.key } // Descending
            .map { ("\($0.key) \($0.key == 1 ? "city" : "cities")", $0.value) }

    case .mostGames:
        return Dictionary(grouping: filteredAndSortedOptions) { $0.totalGames }
            .sorted { $0.key > $1.key }
            .map { ("\($0.key) \($0.key == 1 ? "game" : "games")", $0.value) }

    case .leastGames:
        return Dictionary(grouping: filteredAndSortedOptions) { $0.totalGames }
            .sorted { $0.key < $1.key } // Ascending
            .map { ("\($0.key) \($0.key == 1 ? "game" : "games")", $0.value) }

    case .mostMiles, .leastMiles:
        return groupByMileageRange(ascending: sortOption == .leastMiles)
    }
}

private func groupByMileageRange(ascending: Bool) -> [(String, [ItineraryOption])] {
    let ranges = [(0, 500), (500, 1000), (1000, 1500), (1500, 2000), (2000, Int.max)]
    // Group and format as "0-500 mi", "500-1000 mi", "2000+ mi"
}

UI:

  • Section headers are sticky (standard iOS List behavior)
  • Styled like date headers in Messages app
  • Empty sections hidden after filtering

2. Planning Tips (100+)

New Data File: Core/Data/PlanningTips.swift

struct PlanningTip: Identifiable {
    let id = UUID()
    let icon: String
    let title: String
    let subtitle: String
}

enum PlanningTips {
    static let all: [PlanningTip] = [
        // Schedule timing (~15 tips)
        PlanningTip(icon: "calendar.badge.clock", title: "Check schedules early", subtitle: "Game times can change, sync often"),
        PlanningTip(icon: "clock.badge.exclamationmark", title: "Watch for doubleheaders", subtitle: "Two games, one day—great value!"),

        // Driving/route tips (~15 tips)
        PlanningTip(icon: "car.fill", title: "Plan rest days", subtitle: "Don't overdo the driving"),
        PlanningTip(icon: "fuelpump.fill", title: "Map gas stations", subtitle: "Some stretches are remote"),

        // Stadium-specific (~15 tips)
        PlanningTip(icon: "star.fill", title: "Mark must-sees", subtitle: "Ensure your favorite matchups are included"),
        PlanningTip(icon: "fork.knife", title: "Research stadium food", subtitle: "Some parks have legendary eats"),

        // Budget tips (~15 tips)
        PlanningTip(icon: "dollarsign.circle", title: "Book hotels early", subtitle: "Game days fill up fast"),

        // Multi-sport strategies (~15 tips)
        PlanningTip(icon: "sportscourt.fill", title: "Mix sports", subtitle: "NBA + NHL share many cities"),

        // Regional tips (~15 tips)
        PlanningTip(icon: "cloud.sun.fill", title: "Check the weather", subtitle: "April baseball can be chilly"),

        // Game day tips (~15 tips)
        PlanningTip(icon: "figure.walk", title: "Arrive early", subtitle: "Explore the stadium before first pitch"),

        // ... 90+ more tips
    ]

    static func random(_ count: Int = 3) -> [PlanningTip] {
        Array(all.shuffled().prefix(count))
    }
}

Tip Categories (aim for ~15 each):

  • Schedule timing (rain delays, doubleheaders, TV schedules)
  • Driving/route tips (rest stops, fuel, traffic)
  • Stadium-specific advice (food, parking, views)
  • Budget tips (hotels, tickets, timing)
  • Multi-sport strategies (shared cities, season overlaps)
  • Regional tips (weather, local events, traffic patterns)
  • Game day tips (arrival, gear, photos)

HomeView Integration:

struct HomeView: View {
    @State private var displayedTips: [PlanningTip] = []

    var body: some View {
        // ...
        tipsSection
        // ...
    }

    private var tipsSection: some View {
        ThemedSection(title: "Planning Tips") {
            ForEach(displayedTips) { tip in
                TipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle)
            }
        }
        .onAppear {
            if displayedTips.isEmpty {
                displayedTips = PlanningTips.random(3)
            }
        }
    }
}

Refresh Behavior: Tips refresh on app launch only (not on tab switch).

File Structure

Core/Data/
└── PlanningTips.swift              # NEW - 100+ tips data

Features/Trip/Views/
└── TripCreationView.swift          # MODIFY - TripOptionsView grouped sections

Features/Home/Views/
└── HomeView.swift                  # MODIFY - use PlanningTips.random(3)

Key Decisions

Decision Choice Rationale
Tips refresh timing On app launch only Avoid jarring changes during session
Section collapse No Always expanded, just visual grouping
Empty sections Hide them Don't show "0 games" section
Tip storage Hardcoded array Simple, no CloudKit overhead
Tips count Show 3 Enough variety without overwhelming

Not Included (YAGNI)

  • Tip categories/filtering in UI
  • "See more tips" expansion
  • Tip favoriting/bookmarking
  • CloudKit-synced tips
  • Localized tips (English only for now)

Dependencies

  • None

Testing Considerations

  • Test grouped sorting with various trip counts and distributions
  • Test edge cases: all trips have same city count, only 1 trip
  • Test tips randomization produces different results on relaunch
  • Test section headers are sticky while scrolling