# 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:** ```swift // 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` ```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:** ```swift 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