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>
This commit is contained in:
202
docs/plans/2026-01-12-polish-enhancements-design.md
Normal file
202
docs/plans/2026-01-12-polish-enhancements-design.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 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
|
||||
133
docs/plans/2026-01-12-progress-tracking-enhancements-design.md
Normal file
133
docs/plans/2026-01-12-progress-tracking-enhancements-design.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Progress Tracking Enhancements Design
|
||||
|
||||
**Date:** 2026-01-12
|
||||
**Status:** Draft
|
||||
**Scope:** High-level overview for scoping/prioritization
|
||||
|
||||
## Goal
|
||||
|
||||
Enhance the stadium bucket list experience with richer visit tracking, a dedicated games history view, and an interactive map.
|
||||
|
||||
## Features
|
||||
|
||||
1. **Multiple visits per stadium** - Track and display each visit separately
|
||||
2. **View all games attended** - New screen with chronological list, filterable by year/sport
|
||||
3. **Zoomable map** - Enable pinch-to-zoom and pan on the progress map
|
||||
|
||||
## Current State
|
||||
|
||||
- `StadiumVisit` model already supports multiple visits per stadium (data is there)
|
||||
- UI shows most recent visit, doesn't surface visit history well
|
||||
- `ProgressMapView` has interactions disabled (`interactionModes: []`)
|
||||
- No dedicated "all games attended" view exists
|
||||
|
||||
## Feature Details
|
||||
|
||||
### 1. Multiple Visits Per Stadium
|
||||
|
||||
The `StadiumVisit` model already stores multiple visits. UI changes needed:
|
||||
|
||||
| Component | Change |
|
||||
|-----------|--------|
|
||||
| `VisitDetailView` | Show list of all visits for that stadium, not just most recent |
|
||||
| `StadiumVisitSheet` | "Add Another Visit" button when stadium already has visits |
|
||||
| Stadium cards | Show visit count badge ("3 visits") |
|
||||
|
||||
**UI Flow:**
|
||||
- Tapping a visited stadium shows a list of visit cards
|
||||
- Each visit card is expandable to show game details, photos, notes
|
||||
- "Add Visit" button at bottom of visit list
|
||||
|
||||
### 2. View All Games Attended (GamesHistoryView)
|
||||
|
||||
New screen accessible from Progress tab:
|
||||
|
||||
**Header:** "X Games Attended" with sport filter chips
|
||||
|
||||
**Layout:**
|
||||
- Grouped by year (2026, 2025, 2024...) with sticky section headers
|
||||
- Each row: Date, teams (vs format), stadium, score, sport icon
|
||||
- Tapping a row opens the visit detail
|
||||
|
||||
**Filters:**
|
||||
- Sport: Multi-select chips at top (MLB, NBA, NHL, NFL)
|
||||
- Year: Scroll-based (no separate picker needed)
|
||||
|
||||
**Empty State:** "No games recorded yet. Add your first visit!"
|
||||
|
||||
**Access Points:**
|
||||
- Button in Progress tab header
|
||||
- "See All" link in recent visits section
|
||||
|
||||
### 3. Zoomable Map
|
||||
|
||||
| Current | New |
|
||||
|---------|-----|
|
||||
| `interactionModes: []` | `interactionModes: [.zoom, .pan]` |
|
||||
| Fixed US region | Remembers last position |
|
||||
| Tap shows name label only | Tap zooms to stadium + shows detail card |
|
||||
|
||||
**Additional UI:**
|
||||
- "Reset View" floating button (bottom-right corner)
|
||||
- Appears after user pans or zooms away from default view
|
||||
- Tapping resets to full continental US view
|
||||
|
||||
**Zoom-to-Stadium Behavior:**
|
||||
- Tapping a pin animates map to that stadium
|
||||
- Uses ~0.01 lat/lon span (city level zoom)
|
||||
- Shows stadium detail card below map
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Features/Progress/Views/
|
||||
├── ProgressMapView.swift # MODIFY - enable interactions, add reset button
|
||||
├── VisitDetailView.swift # MODIFY - show all visits as list
|
||||
├── StadiumVisitSheet.swift # MODIFY - "Add Another Visit" flow
|
||||
├── GamesHistoryView.swift # NEW - all games attended screen
|
||||
├── GamesHistoryRow.swift # NEW - single game row component
|
||||
└── VisitListCard.swift # NEW - compact visit card for lists
|
||||
```
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Games history access | Header button + "See All" link | Multiple entry points for discoverability |
|
||||
| Year grouping | Sticky section headers | Standard iOS pattern, easy to scan |
|
||||
| Map zoom on tap | Animate to 0.01 span | City-level detail without losing context |
|
||||
| Reset button visibility | Show only after pan/zoom | Don't clutter UI when at default view |
|
||||
| Visit count badge | Orange circle with number | Consistent with app theme |
|
||||
| Default sort | Most recent first | Users care about recent visits |
|
||||
|
||||
## Data Changes
|
||||
|
||||
No model changes needed:
|
||||
- `StadiumVisit` already supports multiple visits per stadium
|
||||
- `ProgressViewModel` already has `visits` array
|
||||
|
||||
Add computed property to `ProgressViewModel`:
|
||||
```swift
|
||||
var allVisitsByYear: [Int: [VisitSummary]] {
|
||||
Dictionary(grouping: allVisits) { Calendar.current.component(.year, from: $0.visitDate) }
|
||||
}
|
||||
```
|
||||
|
||||
## Not Included (YAGNI)
|
||||
|
||||
- Export games history to CSV
|
||||
- Share individual visit cards to social media
|
||||
- Map clustering for dense stadium areas (revisit if performance issues)
|
||||
- Search within games history (filter chips should suffice)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None - uses existing models and data providers
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
- Test multiple visits for same stadium (add, view, edit)
|
||||
- Test games history with various filter combinations
|
||||
- Test map zoom/pan performance with all stadiums loaded
|
||||
- Test reset button appears/disappears correctly
|
||||
- Test empty states for new users
|
||||
135
docs/plans/2026-01-12-trip-planning-enhancements-design.md
Normal file
135
docs/plans/2026-01-12-trip-planning-enhancements-design.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Trip Planning Enhancements Design
|
||||
|
||||
**Date:** 2026-01-12
|
||||
**Status:** Draft
|
||||
**Scope:** High-level overview for scoping/prioritization
|
||||
|
||||
## Goal
|
||||
|
||||
Transform trip creation from a dense single-form into a guided, progressive-reveal flow that walks users through planning one decision at a time—while keeping everything on a single scrolling screen.
|
||||
|
||||
## Current State
|
||||
|
||||
- Single `TripCreationView` (~2500 lines) with all options visible at once
|
||||
- Planning mode selector controls which sections appear
|
||||
- 4 planning modes: Date Range, Game First, Locations, Follow Team
|
||||
- Sports without games in date range are not indicated
|
||||
|
||||
## Target State
|
||||
|
||||
- Progressive disclosure: sections reveal as user makes selections
|
||||
- Conversational headers guide the user ("Great! When would you like to travel?")
|
||||
- Each section is a small, focused view component (~100-150 lines)
|
||||
- Sports with no games in selected date range are grayed out
|
||||
- Current form remains accessible via Settings toggle for power users
|
||||
|
||||
## Approach: Progressive Single-Screen Flow
|
||||
|
||||
### Container Architecture
|
||||
|
||||
```swift
|
||||
struct TripWizardView: View {
|
||||
@State var viewModel = TripWizardViewModel()
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
// Always visible
|
||||
PlanningModeStep(selection: $viewModel.planningMode)
|
||||
|
||||
// Reveals after planning mode selected
|
||||
if viewModel.planningMode != nil {
|
||||
SportsStep(...)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Reveals after at least one sport selected
|
||||
if !viewModel.selectedSports.isEmpty {
|
||||
DatesStep(...)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Continues for each step...
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.3), value: viewModel.revealState)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step Sequence
|
||||
|
||||
| Step | Question | Reveals When |
|
||||
|------|----------|--------------|
|
||||
| 1 | "How do you want to plan?" | Always visible |
|
||||
| 2 | "Which sports interest you?" | After planning mode selected |
|
||||
| 3 | "When would you like to travel?" | After sport(s) selected |
|
||||
| 4 | "Where do you want to go?" | After dates set |
|
||||
| 5 | "What's your route preference?" | After region(s) selected |
|
||||
| 6 | "Visit cities more than once?" | After route preference |
|
||||
| 7 | "Any must-stop locations?" | After repeat cities choice |
|
||||
| 8 | Review & Plan button | After all required steps complete |
|
||||
|
||||
Note: Step order varies by planning mode (e.g., "Game First" skips dates, shows game picker).
|
||||
|
||||
### Behavior
|
||||
|
||||
- Steps slide in from bottom with fade animation
|
||||
- Auto-scroll to newly revealed section (300ms after animation starts)
|
||||
- User can scroll back up and change earlier answers
|
||||
- Changing an answer resets/updates downstream sections as needed
|
||||
- Conversational headers: "Nice! Now pick your travel dates"
|
||||
|
||||
### Sport Availability (Graying)
|
||||
|
||||
After date selection:
|
||||
1. Fetch game counts per sport for selected date range
|
||||
2. Use existing `AppDataProvider.shared.filterGames(sports:startDate:endDate:)`
|
||||
3. Cache results; re-fetch if dates change
|
||||
4. Display: grayed card + disabled + subtitle "No games in this period"
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Features/Trip/Views/Wizard/
|
||||
├── TripWizardView.swift # Container with progressive reveal logic
|
||||
├── TripWizardViewModel.swift # Shared state across all steps
|
||||
├── Steps/
|
||||
│ ├── PlanningModeStep.swift # "How do you want to plan?"
|
||||
│ ├── SportsStep.swift # Sport grid with availability graying
|
||||
│ ├── DatesStep.swift # Date range picker
|
||||
│ ├── RegionsStep.swift # Region map
|
||||
│ ├── RoutePreferenceStep.swift # Efficient/Scenic/Flexible
|
||||
│ ├── RepeatCitiesStep.swift # Yes/No toggle
|
||||
│ ├── MustStopsStep.swift # Optional locations
|
||||
│ └── ReviewStep.swift # Summary + Plan button
|
||||
```
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Keep old `TripCreationView`? | Yes | Power users, accessible via Settings toggle |
|
||||
| Reset downstream on change? | Yes | Changing sports resets dates, changing dates refetches sport availability |
|
||||
| Sport graying | Grayed + disabled + subtitle | Clear feedback why sport unavailable |
|
||||
| Auto-scroll | 300ms delay after reveal | Let animation start before scrolling |
|
||||
| Step headers | Conversational tone | Guides user, feels less like a form |
|
||||
|
||||
## Not Included (YAGNI)
|
||||
|
||||
- Progress indicator (single screen doesn't need it)
|
||||
- Save draft functionality (overkill for single-session flow)
|
||||
- Undo/redo beyond scrolling back up
|
||||
- Swipe gestures between steps (intentional friction for decisions)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None - uses existing data providers and models
|
||||
- `AppDataProvider.shared.filterGames()` for sport availability
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
- Test progressive reveal with all 4 planning modes
|
||||
- Test sport availability graying with various date ranges
|
||||
- Test downstream reset when changing earlier selections
|
||||
- Test auto-scroll behavior on different screen sizes
|
||||
Reference in New Issue
Block a user