From 3530b31ccaac593ef9dcc965e4c4dfc5d530100e Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 12 Jan 2026 19:07:46 -0600 Subject: [PATCH] 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 --- .../2026-01-12-polish-enhancements-design.md | 202 ++++++++++++++++++ ...2-progress-tracking-enhancements-design.md | 133 ++++++++++++ ...01-12-trip-planning-enhancements-design.md | 135 ++++++++++++ 3 files changed, 470 insertions(+) create mode 100644 docs/plans/2026-01-12-polish-enhancements-design.md create mode 100644 docs/plans/2026-01-12-progress-tracking-enhancements-design.md create mode 100644 docs/plans/2026-01-12-trip-planning-enhancements-design.md diff --git a/docs/plans/2026-01-12-polish-enhancements-design.md b/docs/plans/2026-01-12-polish-enhancements-design.md new file mode 100644 index 0000000..5374856 --- /dev/null +++ b/docs/plans/2026-01-12-polish-enhancements-design.md @@ -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 diff --git a/docs/plans/2026-01-12-progress-tracking-enhancements-design.md b/docs/plans/2026-01-12-progress-tracking-enhancements-design.md new file mode 100644 index 0000000..4579fcb --- /dev/null +++ b/docs/plans/2026-01-12-progress-tracking-enhancements-design.md @@ -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 diff --git a/docs/plans/2026-01-12-trip-planning-enhancements-design.md b/docs/plans/2026-01-12-trip-planning-enhancements-design.md new file mode 100644 index 0000000..6463eb8 --- /dev/null +++ b/docs/plans/2026-01-12-trip-planning-enhancements-design.md @@ -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