- 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>
203 lines
6.8 KiB
Markdown
203 lines
6.8 KiB
Markdown
# 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
|