docs: update planning documents and todos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 13:16:52 -06:00
parent 04b62f147e
commit 3d40145ffb
6 changed files with 2939 additions and 322 deletions

View File

@@ -15,7 +15,12 @@
"Bash(python -m sportstime_parser scrape:*)",
"Bash(pip install:*)",
"Bash(python -m pytest:*)",
"Skill(superpowers:brainstorming)"
"Skill(superpowers:brainstorming)",
"Skill(superpowers:subagent-driven-development)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Skill(superpowers:writing-plans)",
"Bash(find:*)"
]
}
}

View File

@@ -1,19 +1,37 @@
1. organize, optimize, dag system with numerous edge cases with tens of thousands of objects.
```
Read TO-DOS.md in full.
Summarize:
- Goal
- Current Phase
- Active Tasks
Do not write code until this summary is complete.
7. build ios in app purchase for storekit
8. build complete receipt checking system to check if the user has purchased any subscriptions setting a value that can easily be checked anywhere in the app
9. use frontend-design skill to redesign the app
/superpowers:brainstorm todo X <-- will create a design doc in the docs/plan
Issue: Follow team start/end city does not work
Issue: By game gives error date range required but no date range shows
Issue: Date range should always show current selected shit
Issue: By dates with Chicago selected as must stop doesnt find any game
Issue: Im not sure scenario is updating when switched
Issue: The follow team start/end should have the same lookup as must see
Issue: Need to add most cities as sort on all trips views
Issue: Must stop needs to be home team. Just did a search with Chicago as must stop and it had Chi as away team
Issue: Coast to coast on home should filter by most stops
Issue: Importing photo from iPhone, taken with iPhone does not import meta data
Issue: No map should be movable on any screen. It should show North America only and should not be movable
Issue: In schedule view today should be highlighted
Issue: Redesign all loading screens in the app - current loading spinner is ugly and feels unpolished
/superpowers:write-plan for the design created
/superpowers:subagent-driven-development
read docs/TEST_PLAN.md in full
Summarize:
- Goal
- Current Phase
- Active Tasks
Do not write code until this summary is complete.
```
question: do we need sync schedules anymore in settings
// things that are new
need to plan: build ios in app purchase for storekit
need to plan: build complete receipt checking system to check if the user has purchased any subscriptions setting a value that can easily be checked anywhere in the app
need to plan: use frontend-design skill to redesign the app
// new ish to existing features
feature: add the ability to add sports from cloudKit. name, icon, stadiums, etc
// bugs
Issue: sharing looks really dumb. need to be able to share achievements, league progress, and a trip
Issue: fucking game show at 7 am ... the fuck?
Issue: all all trips view when choosing "packed" "moderate" "relaxed" the capsule the option is in does a weird animation that looks off.

View File

@@ -0,0 +1,659 @@
# Polish Enhancements Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add grouped sorting to trip options and expand planning tips to 100+.
**Architecture:** Two independent features: (1) PlanningTips data file with static array and random selection, wired into HomeView; (2) TripOptionsView enhancement to group results by count/range based on sort option.
**Tech Stack:** SwiftUI, Swift
---
## Task 1: Create PlanningTips Data Model
**Files:**
- Create: `SportsTime/Core/Data/PlanningTips.swift`
- Test: `SportsTimeTests/PlanningTipsTests.swift`
**Step 1: Write the failing test for random tip selection**
Create test file:
```swift
//
// PlanningTipsTests.swift
// SportsTimeTests
//
import Testing
@testable import SportsTime
struct PlanningTipsTests {
@Test func allTipsHasAtLeast100Tips() {
#expect(PlanningTips.all.count >= 100)
}
@Test func randomReturnsRequestedCount() {
let tips = PlanningTips.random(3)
#expect(tips.count == 3)
}
@Test func randomReturnsUniqueIds() {
let tips = PlanningTips.random(5)
let uniqueIds = Set(tips.map { $0.id })
#expect(uniqueIds.count == 5)
}
@Test func eachTipHasNonEmptyFields() {
for tip in PlanningTips.all {
#expect(!tip.icon.isEmpty, "Tip should have icon")
#expect(!tip.title.isEmpty, "Tip should have title")
#expect(!tip.subtitle.isEmpty, "Tip should have subtitle")
}
}
@Test func randomWithCountGreaterThanAvailableReturnsAll() {
let tips = PlanningTips.random(1000)
#expect(tips.count == PlanningTips.all.count)
}
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/PlanningTipsTests test`
Expected: FAIL with "Cannot find 'PlanningTips' in scope"
**Step 3: Write the PlanningTips implementation**
Create `SportsTime/Core/Data/PlanningTips.swift`:
```swift
//
// PlanningTips.swift
// SportsTime
//
import Foundation
struct PlanningTip: Identifiable {
let id = UUID()
let icon: String
let title: String
let subtitle: String
}
enum PlanningTips {
static let all: [PlanningTip] = [
// MARK: - 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!"),
PlanningTip(icon: "calendar.badge.plus", title: "Add buffer days", subtitle: "Weather delays happen, plan ahead"),
PlanningTip(icon: "clock.arrow.circlepath", title: "Account for time zones", subtitle: "West coast games start late back east"),
PlanningTip(icon: "tv", title: "Check TV schedules", subtitle: "Nationally broadcast games may shift times"),
PlanningTip(icon: "cloud.rain", title: "Rain delay ready", subtitle: "Baseball can push back your whole evening"),
PlanningTip(icon: "calendar", title: "Avoid holiday weekends", subtitle: "Hotels and tickets spike on long weekends"),
PlanningTip(icon: "sunrise", title: "Day games rock", subtitle: "More time to explore the city after"),
PlanningTip(icon: "moon.stars", title: "Night games = city vibes", subtitle: "Experience the stadium lights atmosphere"),
PlanningTip(icon: "sportscourt", title: "Weekend series strategy", subtitle: "Catch Friday-Saturday-Sunday for max games"),
PlanningTip(icon: "calendar.badge.minus", title: "Check for off-days", subtitle: "Teams travel on Mondays and Thursdays"),
PlanningTip(icon: "exclamationmark.triangle", title: "Spring training caution", subtitle: "Split-squad games mean fewer stars"),
PlanningTip(icon: "clock", title: "First pitch timing", subtitle: "Arrive 60-90 minutes early for batting practice"),
PlanningTip(icon: "hourglass", title: "Game length varies", subtitle: "Baseball: 3hrs, Basketball: 2.5hrs, Hockey: 2.5hrs"),
PlanningTip(icon: "arrow.clockwise", title: "Reschedule alerts", subtitle: "Enable notifications for schedule changes"),
// MARK: - 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"),
PlanningTip(icon: "road.lanes", title: "Avoid rush hour", subtitle: "Leave mid-morning or after 7pm"),
PlanningTip(icon: "map", title: "Scenic route option", subtitle: "Sometimes the long way is the fun way"),
PlanningTip(icon: "bolt.car", title: "EV charging stops", subtitle: "Plan charging during meals for efficiency"),
PlanningTip(icon: "car.side", title: "4-hour driving max", subtitle: "More than 4 hours makes for tired fans"),
PlanningTip(icon: "location.fill", title: "Download offline maps", subtitle: "Cell service can be spotty in rural areas"),
PlanningTip(icon: "gauge.medium", title: "Check tire pressure", subtitle: "Long trips need proper inflation"),
PlanningTip(icon: "wrench.fill", title: "Pre-trip car check", subtitle: "Oil, fluids, wipers—be road ready"),
PlanningTip(icon: "road.lanes.curved.right", title: "Interstate vs scenic", subtitle: "Interstates are faster but highways are prettier"),
PlanningTip(icon: "figure.wave", title: "Stretch breaks", subtitle: "Stop every 2 hours to stay sharp"),
PlanningTip(icon: "cup.and.saucer.fill", title: "Coffee shop stops", subtitle: "Local cafes beat drive-thru every time"),
PlanningTip(icon: "music.note", title: "Curate your playlist", subtitle: "Great music makes miles fly by"),
PlanningTip(icon: "headphones", title: "Podcast the drive", subtitle: "Sports podcasts to get hyped"),
PlanningTip(icon: "car.2.fill", title: "Carpool when possible", subtitle: "Split costs and share the driving"),
// MARK: - 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"),
PlanningTip(icon: "camera.fill", title: "Photo opportunities", subtitle: "Scout iconic spots before game day"),
PlanningTip(icon: "building.2", title: "Stadium tours", subtitle: "Many offer behind-the-scenes access"),
PlanningTip(icon: "figure.walk", title: "Walk the concourse", subtitle: "Each stadium has hidden gems"),
PlanningTip(icon: "bag.fill", title: "Clear bag policy", subtitle: "Most stadiums require clear bags now"),
PlanningTip(icon: "iphone", title: "Mobile tickets ready", subtitle: "Screenshot or download for offline access"),
PlanningTip(icon: "chair.lounge.fill", title: "Seat selection matters", subtitle: "Research views before buying tickets"),
PlanningTip(icon: "sun.max.fill", title: "Shade vs sun seats", subtitle: "Day games: pay extra for shade"),
PlanningTip(icon: "hand.raised.fill", title: "Foul ball territory", subtitle: "Bring a glove to baseline sections"),
PlanningTip(icon: "tshirt.fill", title: "Wear neutral colors", subtitle: "Or proudly rep the home team!"),
PlanningTip(icon: "sparkles", title: "Retro stadium charm", subtitle: "Fenway, Wrigley—experience history"),
PlanningTip(icon: "building.columns", title: "New stadium tech", subtitle: "Modern parks have amazing amenities"),
PlanningTip(icon: "trophy.fill", title: "Championship banners", subtitle: "Look up—history hangs from the rafters"),
PlanningTip(icon: "person.3.fill", title: "Fan sections rock", subtitle: "The bleachers have the best energy"),
// MARK: - Budget Tips (~15 tips)
PlanningTip(icon: "dollarsign.circle", title: "Book hotels early", subtitle: "Game days fill up fast"),
PlanningTip(icon: "ticket.fill", title: "Weekday games cheaper", subtitle: "Tuesday-Thursday = better deals"),
PlanningTip(icon: "creditcard.fill", title: "Set a daily budget", subtitle: "Stadium food adds up quick"),
PlanningTip(icon: "house.fill", title: "Stay outside downtown", subtitle: "Uber in, save on hotels"),
PlanningTip(icon: "bed.double.fill", title: "Points and rewards", subtitle: "Hotel loyalty programs pay off on road trips"),
PlanningTip(icon: "airplane", title: "Compare fly vs drive", subtitle: "Sometimes flying is cheaper than gas"),
PlanningTip(icon: "cart.fill", title: "Grocery runs help", subtitle: "Stock up on snacks, skip concessions"),
PlanningTip(icon: "parkingsign.circle", title: "Pre-book parking", subtitle: "SpotHero and ParkWhiz save money"),
PlanningTip(icon: "percent", title: "Group discounts", subtitle: "4+ people often get bundle deals"),
PlanningTip(icon: "giftcard.fill", title: "Gift card strategy", subtitle: "Buy discounted cards for stadium spending"),
PlanningTip(icon: "arrow.down.circle", title: "Last-minute tickets", subtitle: "Prices drop close to game time"),
PlanningTip(icon: "arrow.up.circle", title: "Book rivalry games early", subtitle: "Yankees-Red Sox sells out fast"),
PlanningTip(icon: "wallet.pass.fill", title: "Season ticket resale", subtitle: "STH often sell below face value"),
PlanningTip(icon: "banknote.fill", title: "Cash for parking", subtitle: "Some lots still don't take cards"),
PlanningTip(icon: "tag.fill", title: "Student/military discounts", subtitle: "Always ask—many teams offer them"),
// MARK: - Multi-Sport Strategies (~15 tips)
PlanningTip(icon: "sportscourt.fill", title: "Mix sports", subtitle: "NBA + NHL share many cities"),
PlanningTip(icon: "calendar.badge.clock", title: "Season overlap sweet spot", subtitle: "April: MLB + NBA + NHL all active"),
PlanningTip(icon: "building.2.fill", title: "Shared arenas", subtitle: "Same building, different sports—efficient!"),
PlanningTip(icon: "figure.basketball", title: "Basketball + hockey combo", subtitle: "Many cities host both in one arena"),
PlanningTip(icon: "baseball.fill", title: "Baseball road trip season", subtitle: "Summer = the classic sports adventure"),
PlanningTip(icon: "hockey.puck.fill", title: "Hockey playoff intensity", subtitle: "April-June games are electric"),
PlanningTip(icon: "basketball.fill", title: "NBA in March", subtitle: "Playoff push means high-stakes games"),
PlanningTip(icon: "leaf.fill", title: "Fall football overlap", subtitle: "NFL Sundays + college Saturdays"),
PlanningTip(icon: "snowflake", title: "Winter sports combo", subtitle: "NBA + NHL peak season in January"),
PlanningTip(icon: "sun.min.fill", title: "Spring training + spring break", subtitle: "Arizona and Florida in March"),
PlanningTip(icon: "figure.run", title: "Cross-sport rivalries", subtitle: "Same city teams often play same weekends"),
PlanningTip(icon: "arrow.triangle.branch", title: "Hub city strategy", subtitle: "Chicago, LA, NYC have multiple teams"),
PlanningTip(icon: "globe.americas.fill", title: "Regional concentration", subtitle: "Northeast has tons of teams close together"),
PlanningTip(icon: "flag.fill", title: "Border city bonus", subtitle: "Toronto + Buffalo = international trip!"),
PlanningTip(icon: "star.circle.fill", title: "All-Star breaks", subtitle: "Avoid—no regular games during break"),
// MARK: - Regional Tips (~15 tips)
PlanningTip(icon: "cloud.sun.fill", title: "Check the weather", subtitle: "April baseball can be chilly"),
PlanningTip(icon: "thermometer.sun.fill", title: "Summer heat warning", subtitle: "Phoenix and Texas get brutal in July"),
PlanningTip(icon: "snow", title: "Winter driving caution", subtitle: "Great Lakes region gets snow early"),
PlanningTip(icon: "wind", title: "Chicago wind factor", subtitle: "Wrigley wind changes the game"),
PlanningTip(icon: "humidity.fill", title: "Southern humidity", subtitle: "Atlanta and Houston: bring water"),
PlanningTip(icon: "mountain.2.fill", title: "Denver altitude", subtitle: "Drink extra water at Coors Field"),
PlanningTip(icon: "beach.umbrella.fill", title: "California weather wins", subtitle: "LA and San Diego = perfect game days"),
PlanningTip(icon: "building.fill", title: "NYC transit tips", subtitle: "Subway to the stadium, skip driving"),
PlanningTip(icon: "tram.fill", title: "Boston T access", subtitle: "Green Line to Fenway, easy as pie"),
PlanningTip(icon: "car.ferry.fill", title: "Seattle ferry option", subtitle: "Combine a game with Puget Sound views"),
PlanningTip(icon: "leaf.arrow.circlepath", title: "Fall foliage bonus", subtitle: "Northeast in October = stunning drives"),
PlanningTip(icon: "theatermasks.fill", title: "Local events check", subtitle: "Festivals can spike hotel prices"),
PlanningTip(icon: "music.mic", title: "Concert conflicts", subtitle: "Arena shows can affect parking"),
PlanningTip(icon: "graduationcap.fill", title: "College town energy", subtitle: "Ann Arbor, Madison—fun atmospheres"),
PlanningTip(icon: "flag.checkered", title: "Racing conflicts", subtitle: "Indy 500 time? Indianapolis books up"),
// MARK: - Game Day Tips (~15 tips)
PlanningTip(icon: "figure.walk", title: "Arrive early", subtitle: "Explore the stadium before first pitch"),
PlanningTip(icon: "drop.fill", title: "Stay hydrated", subtitle: "Especially at outdoor summer games"),
PlanningTip(icon: "sun.max.trianglebadge.exclamationmark", title: "Sunscreen essential", subtitle: "Day games can burn—protect yourself"),
PlanningTip(icon: "battery.100", title: "Charge your phone", subtitle: "You'll want photos and videos"),
PlanningTip(icon: "powerplug.fill", title: "Bring a battery pack", subtitle: "Mobile tickets need power"),
PlanningTip(icon: "speaker.wave.3.fill", title: "Download team app", subtitle: "In-seat ordering, replays, and more"),
PlanningTip(icon: "menucard.fill", title: "Check signature dishes", subtitle: "Every stadium has a must-try item"),
PlanningTip(icon: "clock.badge.checkmark", title: "Gates open timing", subtitle: "Usually 90 minutes before first pitch"),
PlanningTip(icon: "figure.mixed.cardio", title: "Pre-game neighborhood walk", subtitle: "See the local scene before heading in"),
PlanningTip(icon: "wineglass.fill", title: "Pre-game spots", subtitle: "Research nearby bars and restaurants"),
PlanningTip(icon: "gift.fill", title: "Bobblehead nights", subtitle: "Arrive extra early for giveaways"),
PlanningTip(icon: "pencil.and.scribble", title: "Autograph opportunities", subtitle: "Get there for batting practice"),
PlanningTip(icon: "person.badge.plus", title: "Meet other fans", subtitle: "Road trip fans bond fast"),
PlanningTip(icon: "heart.fill", title: "Soak it in", subtitle: "Put the phone down sometimes"),
PlanningTip(icon: "memoryphoto", title: "Take scorecard notes", subtitle: "Physical mementos beat digital"),
]
static func random(_ count: Int = 3) -> [PlanningTip] {
Array(all.shuffled().prefix(count))
}
}
```
**Step 4: Run tests to verify they pass**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/PlanningTipsTests test`
Expected: All 5 tests PASS
**Step 5: Commit**
```bash
git add SportsTime/Core/Data/PlanningTips.swift SportsTimeTests/PlanningTipsTests.swift
git commit -m "$(cat <<'EOF'
feat: add PlanningTips data with 100+ tips
Adds PlanningTip struct and PlanningTips enum with static array
of 105 tips across 7 categories (schedule timing, driving, stadium,
budget, multi-sport, regional, game day). Includes random() function
for selecting N unique tips.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 2: Wire PlanningTips into HomeView
**Files:**
- Modify: `SportsTime/Features/Home/Views/HomeView.swift:311-332`
**Step 1: Add state property and update tipsSection**
In `HomeView.swift`, add a new `@State` property after line 18 (after `selectedSuggestedTrip`):
```swift
@State private var displayedTips: [PlanningTip] = []
```
**Step 2: Replace the hardcoded tipsSection**
Replace lines 313-332 (the entire `tipsSection` computed property) with:
```swift
private var tipsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Planning Tips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
VStack(spacing: Theme.Spacing.xs) {
ForEach(displayedTips) { tip in
TipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle)
}
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
.onAppear {
if displayedTips.isEmpty {
displayedTips = PlanningTips.random(3)
}
}
}
```
**Step 3: Run the app to verify tips load**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds. Launch app and verify 3 random tips appear in the Planning Tips section.
**Step 4: Commit**
```bash
git add SportsTime/Features/Home/Views/HomeView.swift
git commit -m "$(cat <<'EOF'
feat: wire PlanningTips into HomeView
- Add displayedTips @State to HomeView
- Update tipsSection to use PlanningTips.random(3)
- Tips load on first appear, persist during session
- Tips refresh only on app relaunch
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 3: Add Grouped Sorting to TripOptionsView
**Files:**
- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift:1570-1839`
- Test: `SportsTimeTests/TripOptionsGroupingTests.swift`
**Step 1: Write failing test for grouping logic**
Create test file:
```swift
//
// TripOptionsGroupingTests.swift
// SportsTimeTests
//
import Testing
@testable import SportsTime
struct TripOptionsGroupingTests {
// Helper to create mock ItineraryOption
private func makeOption(stops: [(city: String, games: [String])], totalMiles: Double = 500) -> ItineraryOption {
let tripStops = stops.map { stopData in
TripStop(
city: stopData.city,
state: "XX",
coordinate: .init(latitude: 0, longitude: 0),
games: stopData.games,
arrivalDate: Date(),
departureDate: Date(),
travelFromPrevious: nil
)
}
return ItineraryOption(
id: UUID().uuidString,
stops: tripStops,
totalDistanceMiles: totalMiles,
totalDrivingHours: totalMiles / 60,
score: 1.0
)
}
@Test func groupByCityCountDescending() {
let options = [
makeOption(stops: [("NYC", []), ("Boston", [])]), // 2 cities
makeOption(stops: [("LA", []), ("SF", []), ("Seattle", [])]), // 3 cities
makeOption(stops: [("Chicago", [])]), // 1 city
]
let grouped = TripOptionsGrouper.groupByCityCount(options, ascending: false)
#expect(grouped.count == 3)
#expect(grouped[0].header == "3 cities")
#expect(grouped[1].header == "2 cities")
#expect(grouped[2].header == "1 city")
}
@Test func groupByGameCountAscending() {
let options = [
makeOption(stops: [("NYC", ["g1", "g2", "g3"])]), // 3 games
makeOption(stops: [("LA", ["g1"])]), // 1 game
makeOption(stops: [("Chicago", ["g1", "g2"])]), // 2 games
]
let grouped = TripOptionsGrouper.groupByGameCount(options, ascending: true)
#expect(grouped.count == 3)
#expect(grouped[0].header == "1 game")
#expect(grouped[1].header == "2 games")
#expect(grouped[2].header == "3 games")
}
@Test func groupByMileageRangeDescending() {
let options = [
makeOption(stops: [("NYC", [])], totalMiles: 300), // 0-500
makeOption(stops: [("LA", [])], totalMiles: 1200), // 1000-1500
makeOption(stops: [("Chicago", [])], totalMiles: 2500), // 2000+
]
let grouped = TripOptionsGrouper.groupByMileageRange(options, ascending: false)
#expect(grouped[0].header == "2000+ mi")
#expect(grouped[1].header == "1000-1500 mi")
#expect(grouped[2].header == "0-500 mi")
}
@Test func groupByMileageRangeAscending() {
let options = [
makeOption(stops: [("NYC", [])], totalMiles: 300),
makeOption(stops: [("LA", [])], totalMiles: 1200),
makeOption(stops: [("Chicago", [])], totalMiles: 2500),
]
let grouped = TripOptionsGrouper.groupByMileageRange(options, ascending: true)
#expect(grouped[0].header == "0-500 mi")
#expect(grouped[1].header == "1000-1500 mi")
#expect(grouped[2].header == "2000+ mi")
}
@Test func emptyOptionsReturnsEmptyGroups() {
let grouped = TripOptionsGrouper.groupByCityCount([], ascending: false)
#expect(grouped.isEmpty)
}
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TripOptionsGroupingTests test`
Expected: FAIL with "Cannot find 'TripOptionsGrouper' in scope"
**Step 3: Add TripOptionsGrouper utility**
Add this before the `TripOptionsView` struct (around line 1570) in `TripCreationView.swift`:
```swift
// MARK: - Trip Options Grouper
enum TripOptionsGrouper {
typealias GroupedOptions = (header: String, options: [ItineraryOption])
static func groupByCityCount(_ options: [ItineraryOption], ascending: Bool) -> [GroupedOptions] {
let grouped = Dictionary(grouping: options) { option in
Set(option.stops.map { $0.city }).count
}
let sorted = ascending ? grouped.sorted { $0.key < $1.key } : grouped.sorted { $0.key > $1.key }
return sorted.map { count, opts in
("\(count) \(count == 1 ? "city" : "cities")", opts)
}
}
static func groupByGameCount(_ options: [ItineraryOption], ascending: Bool) -> [GroupedOptions] {
let grouped = Dictionary(grouping: options) { $0.totalGames }
let sorted = ascending ? grouped.sorted { $0.key < $1.key } : grouped.sorted { $0.key > $1.key }
return sorted.map { count, opts in
("\(count) \(count == 1 ? "game" : "games")", opts)
}
}
static func groupByMileageRange(_ options: [ItineraryOption], ascending: Bool) -> [GroupedOptions] {
let ranges: [(min: Int, max: Int, label: String)] = [
(0, 500, "0-500 mi"),
(500, 1000, "500-1000 mi"),
(1000, 1500, "1000-1500 mi"),
(1500, 2000, "1500-2000 mi"),
(2000, Int.max, "2000+ mi")
]
var groupedDict: [String: [ItineraryOption]] = [:]
for option in options {
let miles = Int(option.totalDistanceMiles)
for range in ranges {
if miles >= range.min && miles < range.max {
groupedDict[range.label, default: []].append(option)
break
}
}
}
// Sort by range order
let rangeOrder = ascending ? ranges : ranges.reversed()
return rangeOrder.compactMap { range in
guard let opts = groupedDict[range.label], !opts.isEmpty else { return nil }
return (range.label, opts)
}
}
}
```
**Step 4: Run tests to verify they pass**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TripOptionsGroupingTests test`
Expected: All 5 tests PASS
**Step 5: Commit**
```bash
git add SportsTime/Features/Trip/Views/TripCreationView.swift SportsTimeTests/TripOptionsGroupingTests.swift
git commit -m "$(cat <<'EOF'
feat: add TripOptionsGrouper utility for grouped sorting
Adds grouping functions for city count, game count, and mileage range
with ascending/descending support. Used by TripOptionsView for
sectioned display based on sort option.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 4: Integrate Grouped Sections into TripOptionsView UI
**Files:**
- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift:1570-1839` (TripOptionsView)
**Step 1: Add groupedOptions computed property to TripOptionsView**
Add this computed property after `filteredAndSortedOptions` (around line 1635):
```swift
private var groupedOptions: [TripOptionsGrouper.GroupedOptions] {
switch sortOption {
case .recommended, .bestEfficiency:
// Flat list, no grouping
return [("", filteredAndSortedOptions)]
case .mostCities:
return TripOptionsGrouper.groupByCityCount(filteredAndSortedOptions, ascending: false)
case .mostGames:
return TripOptionsGrouper.groupByGameCount(filteredAndSortedOptions, ascending: false)
case .leastGames:
return TripOptionsGrouper.groupByGameCount(filteredAndSortedOptions, ascending: true)
case .mostMiles:
return TripOptionsGrouper.groupByMileageRange(filteredAndSortedOptions, ascending: false)
case .leastMiles:
return TripOptionsGrouper.groupByMileageRange(filteredAndSortedOptions, ascending: true)
}
}
```
**Step 2: Replace the options list with sectioned display**
In `TripOptionsView.body`, replace the options list section (the ForEach for `filteredAndSortedOptions` around lines 1668-1679) with:
```swift
// Options list (grouped when applicable)
if filteredAndSortedOptions.isEmpty {
emptyFilterState
.padding(.top, Theme.Spacing.xl)
} else {
ForEach(Array(groupedOptions.enumerated()), id: \.offset) { _, group in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Section header (only if non-empty)
if !group.header.isEmpty {
HStack {
Text(group.header)
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Text("\(group.options.count)")
.font(.subheadline)
.foregroundStyle(Theme.textMuted(colorScheme))
}
.padding(.horizontal, Theme.Spacing.md)
.padding(.top, Theme.Spacing.md)
}
// Options in this group
ForEach(group.options) { option in
TripOptionCard(
option: option,
games: games,
onSelect: {
selectedTrip = convertToTrip(option)
showTripDetail = true
}
)
.padding(.horizontal, Theme.Spacing.md)
}
}
}
}
```
**Step 3: Build and verify UI**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds. Run app, plan a trip, verify grouped sections appear when sorting by Most Cities, Most Games, etc.
**Step 4: Commit**
```bash
git add SportsTime/Features/Trip/Views/TripCreationView.swift
git commit -m "$(cat <<'EOF'
feat: add grouped sections to TripOptionsView
- Add groupedOptions computed property using TripOptionsGrouper
- Update UI to show section headers with counts when grouping
- Recommended and Best Efficiency remain flat lists
- Empty sections are automatically hidden
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 5: Run Full Test Suite and Final Verification
**Files:**
- None (verification only)
**Step 1: Run full test suite**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: All tests pass (including new PlanningTipsTests and TripOptionsGroupingTests)
**Step 2: Verify features manually**
1. Launch app
2. Check Home tab → Planning Tips shows 3 random tips
3. Kill app, relaunch → verify tips changed
4. Plan a trip with multiple options
5. On TripOptionsView:
- Select "Most Cities" → verify grouped by city count descending
- Select "Least Games" → verify grouped by game count ascending
- Select "Most Miles" → verify grouped by mileage ranges
- Select "Recommended" → verify flat list (no headers)
**Step 3: Final commit with all changes**
```bash
git status
# Verify all changes are committed
```
Expected: Working tree clean, all features implemented and tested.
---
## Summary
| Task | Description | Tests |
|------|-------------|-------|
| 1 | Create PlanningTips data model with 100+ tips | 5 tests |
| 2 | Wire PlanningTips into HomeView | Manual verification |
| 3 | Add TripOptionsGrouper utility | 5 tests |
| 4 | Integrate grouped sections into TripOptionsView UI | Manual verification |
| 5 | Full test suite run and verification | All tests pass |
**Total new tests:** 10
**Files created:** 2 (`PlanningTips.swift`, `PlanningTipsTests.swift`, `TripOptionsGroupingTests.swift`)
**Files modified:** 2 (`HomeView.swift`, `TripCreationView.swift`)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,799 @@
# TODO Bug Fixes Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Fix 9 bugs affecting app performance, UI polish, and data pipeline.
**Architecture:** Refactor `SuggestedTripsGenerator` to run heavy work off main actor; add pagination to large lists; fix UI display issues and animations; add stadium timezone support; add scraper alias.
**Tech Stack:** Swift, SwiftUI, @Observable, async/await, Python (scraper)
---
## Task 1: Fix Launch Freeze (Bug 1)
**Files:**
- Modify: `SportsTime/Core/Services/SuggestedTripsGenerator.swift`
**Step 1: Identify the blocking code**
The entire `generateTrips()` method runs on `@MainActor`. We need to move heavy computation to a background task while keeping UI updates on main.
**Step 2: Create a non-isolated helper struct for planning**
Add this struct above `SuggestedTripsGenerator`:
```swift
// Non-isolated helper for background trip generation
private struct TripGenerationHelper: Sendable {
let stadiums: [Stadium]
let teams: [Team]
let games: [Game]
func generateRegionalTrips(
startDate: Date,
endDate: Date
) -> [SuggestedTrip] {
let planningEngine = TripPlanningEngine()
let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 }
let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 }
var generatedTrips: [SuggestedTrip] = []
// Regional trips (copy existing logic from generateTrips)
for region in [Region.east, Region.central, Region.west] {
let regionStadiumIds = Set(
stadiums
.filter { $0.region == region }
.map { $0.id }
)
let regionGames = games.filter { regionStadiumIds.contains($0.stadiumId) }
guard !regionGames.isEmpty else { continue }
// Generate trips using existing private methods (will need to move them)
}
return generatedTrips
}
}
```
**Step 3: Refactor generateTrips() to use Task.detached**
Replace the heavy computation section in `generateTrips()`:
```swift
func generateTrips() async {
guard !isLoading else { return }
isLoading = true
error = nil
suggestedTrips = []
loadingMessage = await loadingTextGenerator.generateMessage()
// Ensure data is loaded
if dataProvider.teams.isEmpty {
await dataProvider.loadInitialData()
}
guard !dataProvider.stadiums.isEmpty else {
error = "Unable to load stadium data"
isLoading = false
return
}
let calendar = Calendar.current
let today = Date()
guard let startDate = calendar.date(byAdding: .weekOfYear, value: 4, to: today),
let endDate = calendar.date(byAdding: .weekOfYear, value: 8, to: today) else {
error = "Failed to calculate date range"
isLoading = false
return
}
do {
let allSports = Set(Sport.supported)
let games = try await dataProvider.filterGames(
sports: allSports,
startDate: startDate,
endDate: endDate
)
guard !games.isEmpty else {
error = "No games found in the next 4-8 weeks"
isLoading = false
return
}
// Capture data for background work
let stadiums = dataProvider.stadiums
let teams = dataProvider.teams
// Heavy computation off main actor
let generatedTrips = await Task.detached(priority: .userInitiated) {
self.generateTripsInBackground(
games: games,
stadiums: stadiums,
teams: teams,
startDate: startDate,
endDate: endDate
)
}.value
suggestedTrips = generatedTrips
} catch {
self.error = "Failed to generate trips: \(error.localizedDescription)"
}
isLoading = false
}
// New nonisolated method for background work
nonisolated private func generateTripsInBackground(
games: [Game],
stadiums: [Stadium],
teams: [Team],
startDate: Date,
endDate: Date
) -> [SuggestedTrip] {
let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 }
let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 }
var generatedTrips: [SuggestedTrip] = []
// Copy existing regional trip generation logic here...
// (Move the for loop from the original generateTrips)
return generatedTrips
}
```
**Step 4: Move helper methods to be nonisolated**
Mark these methods as `nonisolated`:
- `generateRegionalTrip`
- `generateCrossCountryTrip`
- `buildRichGames`
- `buildCoastToCoastRoute`
- `buildTripStop`
- `buildTravelSegments`
- `convertToTrip`
- `generateTripName`
- `buildCorridorTrip`
- `haversineDistance`
- `validateNoSameDayConflicts`
**Step 5: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
Expected: Build succeeds
**Step 6: Manual test**
Launch app in simulator. UI should be responsive immediately while "Suggested Trips" section shows loading state.
**Step 7: Commit**
```bash
git add SportsTime/Core/Services/SuggestedTripsGenerator.swift
git commit -m "perf: move trip generation off main actor
Fixes launch freeze by running TripPlanningEngine in background task.
UI now responsive immediately while trips generate."
```
---
## Task 2: Add Location Info to "By Game" View (Bug 3)
**Files:**
- Modify: `SportsTime/Features/Schedule/Views/ScheduleListView.swift`
**Step 1: Find GameRowView and add showLocation parameter**
Locate `GameRowView` in the file (or it may be in a separate file). Add parameter:
```swift
struct GameRowView: View {
let game: RichGame
var showDate: Bool = false
var showLocation: Bool = false // Add this
```
**Step 2: Add location display in the view body**
After the existing content, add:
```swift
var body: some View {
VStack(alignment: .leading, spacing: 4) {
// ... existing matchup content
if showLocation {
HStack(spacing: 4) {
Image(systemName: "mappin.circle.fill")
.font(.caption2)
.foregroundStyle(.tertiary)
Text("\(game.stadium.name), \(game.stadium.city)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
```
**Step 3: Pass showLocation when groupBy is byGame**
Find where `GameRowView` is called and update:
```swift
// When rendering games in "by game" mode:
GameRowView(game: richGame, showDate: true, showLocation: groupBy == .byGame)
```
**Step 4: Build and verify**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 5: Manual test**
Open Schedule tab, switch to "By Game" grouping. Each row should now show stadium name and city.
**Step 6: Commit**
```bash
git add SportsTime/Features/Schedule/Views/ScheduleListView.swift
git commit -m "feat: show location info in By Game schedule view
Adds stadium name and city to game rows when grouped by game."
```
---
## Task 3: Fix Game Selection Animation (Bug 5)
**Files:**
- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift`
**Step 1: Find the game selection tap handler**
Look for the game selection list/grid and its tap action.
**Step 2: Wrap selection in transaction to disable animation**
```swift
// Replace direct toggle:
// viewModel.toggleGameSelection(game)
// With:
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
viewModel.toggleGameSelection(game)
}
```
**Step 3: Alternative - add animation nil modifier**
If using a selection indicator (checkmark), add:
```swift
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isSelected ? .blue : .secondary)
.animation(nil, value: isSelected)
```
**Step 4: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 5: Manual test**
Go to trip creation, select "By Game" mode, tap games to select. Animation should be instant without weird morphing.
**Step 6: Commit**
```bash
git add SportsTime/Features/Trip/Views/TripCreationView.swift
git commit -m "fix: remove weird animation on game selection
Disables implicit animation when toggling game selection."
```
---
## Task 4: Fix Pace Capsule Animation (Bug 7)
**Files:**
- Modify: `SportsTime/Features/Home/Views/SavedTripsListView.swift` (or similar)
**Step 1: Find the pace capsule component**
Search for "packed", "moderate", "relaxed" or pace-related text in the trips list view.
**Step 2: Add contentTransition and disable animation**
```swift
Text(trip.paceLabel) // or whatever the pace text is
.font(.caption2)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(paceColor.opacity(0.2))
.foregroundStyle(paceColor)
.clipShape(Capsule())
.contentTransition(.identity) // Prevents text morphing
.animation(nil, value: trip.id) // No animation on data change
```
**Step 3: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 4: Manual test**
View saved trips list. Pace capsules should appear without glitchy animation.
**Step 5: Commit**
```bash
git add SportsTime/Features/Home/Views/SavedTripsListView.swift
git commit -m "fix: pace capsule animation glitch
Adds contentTransition(.identity) to prevent text morphing."
```
---
## Task 5: Remove "My Trips" Title (Bug 8)
**Files:**
- Modify: `SportsTime/Features/Home/Views/HomeView.swift` (or main TabView file)
**Step 1: Find the My Trips tab**
Look for TabView and the "My Trips" tab item.
**Step 2: Remove or hide navigation title**
Option A - Remove `.navigationTitle()`:
```swift
NavigationStack {
SavedTripsListView(trips: savedTrips)
// Remove: .navigationTitle("My Trips")
}
.tabItem {
Label("My Trips", systemImage: "suitcase")
}
```
Option B - Hide navigation bar:
```swift
NavigationStack {
SavedTripsListView(trips: savedTrips)
.toolbar(.hidden, for: .navigationBar)
}
.tabItem {
Label("My Trips", systemImage: "suitcase")
}
```
**Step 3: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 4: Manual test**
Switch to My Trips tab. No redundant title should appear above the content.
**Step 5: Commit**
```bash
git add SportsTime/Features/Home/Views/HomeView.swift
git commit -m "fix: remove redundant My Trips title from tab view"
```
---
## Task 6: Add Stadium Timezone Support (Bug 6)
**Files:**
- Modify: `SportsTime/Core/Models/Domain/Stadium.swift`
- Modify: `SportsTime/Core/Models/Domain/Game.swift`
- Modify: `Scripts/sportstime_parser/normalizers/stadium_resolver.py`
**Step 1: Add timeZoneIdentifier to Stadium model**
```swift
struct Stadium: Identifiable, Codable, Hashable {
let id: String
let name: String
let city: String
let state: String
let coordinate: CLLocationCoordinate2D
let sport: Sport
let region: Region
let timeZoneIdentifier: String? // Add this
var timeZone: TimeZone? {
timeZoneIdentifier.flatMap { TimeZone(identifier: $0) }
}
// Update init to include timeZoneIdentifier with default nil for backward compatibility
init(
id: String,
name: String,
city: String,
state: String,
coordinate: CLLocationCoordinate2D,
sport: Sport,
region: Region,
timeZoneIdentifier: String? = nil
) {
self.id = id
self.name = name
self.city = city
self.state = state
self.coordinate = coordinate
self.sport = sport
self.region = region
self.timeZoneIdentifier = timeZoneIdentifier
}
}
```
**Step 2: Add localGameTime to RichGame**
```swift
extension RichGame {
var localGameTime: String {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a z"
formatter.timeZone = stadium.timeZone ?? .current
return formatter.string(from: game.dateTime)
}
var localGameTimeShort: String {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
formatter.timeZone = stadium.timeZone ?? .current
return formatter.string(from: game.dateTime)
}
}
```
**Step 3: Update Python scraper StadiumInfo**
In `stadium_resolver.py`, update the dataclass and all stadium entries:
```python
@dataclass
class StadiumInfo:
canonical_id: str
name: str
city: str
state: str
country: str
sport: str
latitude: float
longitude: float
timezone: str = "America/New_York" # Add with default
# Update entries, e.g.:
"stadium_nba_chase_center": StadiumInfo("stadium_nba_chase_center", "Chase Center", "San Francisco", "CA", "USA", "nba", 37.7680, -122.3877, "America/Los_Angeles"),
```
**Step 4: Build Swift code**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 5: Commit**
```bash
git add SportsTime/Core/Models/Domain/Stadium.swift SportsTime/Core/Models/Domain/Game.swift Scripts/sportstime_parser/normalizers/stadium_resolver.py
git commit -m "feat: add timezone support for stadium-local game times
Adds timeZoneIdentifier to Stadium and localGameTime to RichGame.
Game times can now display in venue local time."
```
**Note:** Full timezone population for all stadiums is a separate data task.
---
## Task 7: Add Frost Bank Center Alias (Bug 9)
**Files:**
- Modify: `Scripts/stadium_aliases.json`
**Step 1: Open stadium_aliases.json and add alias**
Add entry for AT&T Center (old name for Frost Bank Center):
```json
{
"canonical_id": "stadium_nba_frost_bank_center",
"alias_name": "AT&T Center",
"sport": "nba",
"valid_from": "2002-10-18",
"valid_to": "2024-07-01",
"notes": "Renamed to Frost Bank Center in July 2024"
}
```
**Step 2: Verify JSON is valid**
Run:
```bash
python3 -c "import json; json.load(open('Scripts/stadium_aliases.json'))"
```
Expected: No output (valid JSON)
**Step 3: Test scraper resolution**
Run:
```bash
cd Scripts && python3 -c "
from sportstime_parser.normalizers.stadium_resolver import resolve_stadium
result = resolve_stadium('nba', 'AT&T Center')
print(f'Resolved: {result.canonical_id}, confidence: {result.confidence}')
"
```
Expected: `Resolved: stadium_nba_frost_bank_center, confidence: 95`
**Step 4: Commit**
```bash
git add Scripts/stadium_aliases.json
git commit -m "fix: add AT&T Center alias for Frost Bank Center
NBA scraper now resolves old stadium name to current Spurs arena."
```
---
## Task 8: Add Pagination to Schedule List (Bug 2)
**Files:**
- Modify: `SportsTime/Features/Schedule/Views/ScheduleListView.swift`
- Modify: `SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift`
**Step 1: Add pagination state to ViewModel**
```swift
@Observable
class ScheduleViewModel {
// Existing properties...
// Pagination
private let pageSize = 50
var displayedGames: [RichGame] = []
private var currentPage = 0
private var allFilteredGames: [RichGame] = []
var hasMoreGames: Bool {
displayedGames.count < allFilteredGames.count
}
func loadInitialGames() {
currentPage = 0
displayedGames = Array(allFilteredGames.prefix(pageSize))
}
func loadMoreGames() {
guard hasMoreGames else { return }
currentPage += 1
let start = currentPage * pageSize
let end = min(start + pageSize, allFilteredGames.count)
displayedGames.append(contentsOf: allFilteredGames[start..<end])
}
}
```
**Step 2: Update filtering to populate allFilteredGames**
In the method that filters/fetches games:
```swift
func applyFilters() async {
// ... existing filter logic that produces games
allFilteredGames = filteredGames // Store full list
loadInitialGames() // Display first page
}
```
**Step 3: Update ScheduleListView to use displayedGames**
```swift
struct ScheduleListView: View {
@Bindable var viewModel: ScheduleViewModel
var body: some View {
List {
ForEach(viewModel.displayedGames) { game in
GameRowView(game: game, showDate: true, showLocation: viewModel.groupBy == .byGame)
.onAppear {
if game.id == viewModel.displayedGames.last?.id {
viewModel.loadMoreGames()
}
}
}
if viewModel.hasMoreGames {
ProgressView()
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
}
}
}
```
**Step 4: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 5: Manual test**
Open Schedule tab with "By Game" grouping. List should load quickly with 50 games, then load more as you scroll.
**Step 6: Commit**
```bash
git add SportsTime/Features/Schedule/Views/ScheduleListView.swift SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift
git commit -m "perf: add pagination to schedule list
Loads 50 games at a time to fix lag with large datasets."
```
---
## Task 9: Fix Game Selection View Performance (Bug 4)
**Files:**
- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift`
- Modify: `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift`
**Step 1: Add pagination to TripCreationViewModel**
Similar to Task 8, add pagination for the game selection:
```swift
// In TripCreationViewModel
private let gamePageSize = 50
var displayedAvailableGames: [RichGame] = []
private var currentGamePage = 0
var hasMoreAvailableGames: Bool {
displayedAvailableGames.count < availableGames.count
}
func loadInitialAvailableGames() {
currentGamePage = 0
displayedAvailableGames = Array(availableGames.prefix(gamePageSize))
}
func loadMoreAvailableGames() {
guard hasMoreAvailableGames else { return }
currentGamePage += 1
let start = currentGamePage * gamePageSize
let end = min(start + gamePageSize, availableGames.count)
displayedAvailableGames.append(contentsOf: availableGames[start..<end])
}
```
**Step 2: Update game selection view to use displayedAvailableGames**
```swift
// In game selection section of TripCreationView
LazyVStack(spacing: 8) {
ForEach(viewModel.displayedAvailableGames) { game in
GameSelectionRow(game: game, isSelected: viewModel.selectedGameIds.contains(game.id))
.onTapGesture {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
viewModel.toggleGameSelection(game)
}
}
.onAppear {
if game.id == viewModel.displayedAvailableGames.last?.id {
viewModel.loadMoreAvailableGames()
}
}
}
}
```
**Step 3: Fix color inconsistency**
Ensure all game rows use consistent background:
```swift
struct GameSelectionRow: View {
let game: RichGame
let isSelected: Bool
var body: some View {
HStack {
// Game info...
Spacer()
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isSelected ? .blue : .secondary)
}
.padding()
.background(Color(.systemBackground)) // Consistent background
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
```
**Step 4: Build and test**
Run:
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
**Step 5: Manual test**
Create a trip with "By Game" mode. Game selection should be smooth and colors consistent.
**Step 6: Commit**
```bash
git add SportsTime/Features/Trip/Views/TripCreationView.swift SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift
git commit -m "perf: add pagination to game selection view
Fixes lag and color inconsistency in trip creation game selection."
```
---
## Final Task: Run Full Test Suite
**Step 1: Run all tests**
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
```
**Step 2: Verify all pass**
Expected: All tests pass (existing 63 tests)
**Step 3: Manual smoke test**
- [ ] Launch app - UI responsive immediately
- [ ] Home tab - suggested trips load in background
- [ ] Schedule tab - "By Game" shows location, scrolls smoothly
- [ ] Trip creation - game selection smooth, no weird animation
- [ ] My Trips - no redundant title, pace capsules look good

View File

@@ -47,20 +47,13 @@ Inside "Pro Access" group, click "+" → Add Subscription:
Edit Scheme → Run → Options → StoreKit Configuration → Select `SportsTime.storekit`
**Step 6: Commit**
```bash
git add SportsTime/SportsTime.storekit SportsTime.xcodeproj
git commit -m "feat(iap): add StoreKit configuration file with Pro subscriptions"
```
---
## Task 2: Create ProFeature Enum
**Files:**
- Create: `SportsTime/SportsTime/Core/Store/ProFeature.swift`
- Test: `SportsTime/SportsTimeTests/Store/ProFeatureTests.swift`
- Create: `SportsTime/SportsTimeTests/Store/ProFeatureTests.swift`
**Step 1: Write the test**
@@ -104,13 +97,7 @@ struct ProFeatureTests {
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ProFeatureTests test`
Expected: FAIL with "cannot find 'ProFeature' in scope"
**Step 3: Write minimal implementation**
**Step 2: Write implementation**
Create `SportsTime/SportsTime/Core/Store/ProFeature.swift`:
@@ -160,26 +147,13 @@ enum ProFeature: String, CaseIterable, Identifiable {
}
```
**Step 4: Run test to verify it passes**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ProFeatureTests test`
Expected: PASS
**Step 5: Commit**
```bash
git add SportsTime/SportsTime/Core/Store/ProFeature.swift SportsTime/SportsTimeTests/Store/ProFeatureTests.swift
git commit -m "feat(iap): add ProFeature enum defining gated features"
```
---
## Task 3: Create StoreError Enum
**Files:**
- Create: `SportsTime/SportsTime/Core/Store/StoreError.swift`
- Test: `SportsTime/SportsTimeTests/Store/StoreErrorTests.swift`
- Create: `SportsTime/SportsTimeTests/Store/StoreErrorTests.swift`
**Step 1: Write the test**
@@ -203,13 +177,7 @@ struct StoreErrorTests {
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/StoreErrorTests test`
Expected: FAIL
**Step 3: Write minimal implementation**
**Step 2: Write implementation**
Create `SportsTime/SportsTime/Core/Store/StoreError.swift`:
@@ -242,26 +210,13 @@ enum StoreError: LocalizedError {
}
```
**Step 4: Run test to verify it passes**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/StoreErrorTests test`
Expected: PASS
**Step 5: Commit**
```bash
git add SportsTime/SportsTime/Core/Store/StoreError.swift SportsTime/SportsTimeTests/Store/StoreErrorTests.swift
git commit -m "feat(iap): add StoreError enum for purchase error handling"
```
---
## Task 4: Create StoreManager Core
**Files:**
- Create: `SportsTime/SportsTime/Core/Store/StoreManager.swift`
- Test: `SportsTime/SportsTimeTests/Store/StoreManagerTests.swift`
- Create: `SportsTime/SportsTimeTests/Store/StoreManagerTests.swift`
**Step 1: Write the test**
@@ -303,13 +258,7 @@ struct StoreManagerTests {
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/StoreManagerTests test`
Expected: FAIL
**Step 3: Write minimal implementation**
**Step 2: Write implementation**
Create `SportsTime/SportsTime/Core/Store/StoreManager.swift`:
@@ -458,19 +407,6 @@ final class StoreManager {
}
```
**Step 4: Run test to verify it passes**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/StoreManagerTests test`
Expected: PASS
**Step 5: Commit**
```bash
git add SportsTime/SportsTime/Core/Store/StoreManager.swift SportsTime/SportsTimeTests/Store/StoreManagerTests.swift
git commit -m "feat(iap): add StoreManager with StoreKit 2 entitlement checking"
```
---
## Task 5: Create ProBadge View
@@ -531,13 +467,6 @@ extension View {
}
```
**Step 2: Commit**
```bash
git add SportsTime/SportsTime/Features/Paywall/Views/ProBadge.swift
git commit -m "feat(iap): add ProBadge view for locked feature indicators"
```
---
## Task 6: Create PaywallView
@@ -863,20 +792,13 @@ struct PricingOptionCard: View {
}
```
**Step 2: Commit**
```bash
git add SportsTime/SportsTime/Features/Paywall/Views/PaywallView.swift
git commit -m "feat(iap): add PaywallView with pricing options and purchase flow"
```
---
## Task 7: Create ProGate View Modifier
**Files:**
- Create: `SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift`
- Test: `SportsTime/SportsTimeTests/Store/ProGateTests.swift`
- Create: `SportsTime/SportsTimeTests/Store/ProGateTests.swift`
**Step 1: Write the test**
@@ -901,13 +823,7 @@ struct ProGateTests {
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ProGateTests test`
Expected: FAIL
**Step 3: Write minimal implementation**
**Step 2: Write implementation**
Create `SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift`:
@@ -985,19 +901,6 @@ extension View {
}
```
**Step 4: Run test to verify it passes**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ProGateTests test`
Expected: PASS
**Step 5: Commit**
```bash
git add SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift SportsTime/SportsTimeTests/Store/ProGateTests.swift
git commit -m "feat(iap): add ProGate view modifier for feature gating"
```
---
## Task 8: Create OnboardingPaywallView
@@ -1318,23 +1221,16 @@ struct OnboardingPricingRow: View {
}
```
**Step 2: Commit**
```bash
git add SportsTime/SportsTime/Features/Paywall/Views/OnboardingPaywallView.swift
git commit -m "feat(iap): add OnboardingPaywallView with feature pages and pricing"
```
---
## Task 9: Integrate StoreManager in App Lifecycle
**Files:**
- Modify: `SportsTime/SportsTime/SportsTimeApp.swift:46-53` (add transaction listener)
- Modify: `SportsTime/SportsTime/SportsTimeApp.swift`
**Step 1: Add transaction listener property and initialization**
In `SportsTimeApp.swift`, add after line 45 (after `sharedModelContainer`):
In `SportsTimeApp.swift`, add after `sharedModelContainer` (around line 45):
```swift
/// Task that listens for StoreKit transaction updates
@@ -1393,25 +1289,12 @@ Update the `body` to show onboarding paywall:
}
```
**Step 5: Run full test suite**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: All tests PASS
**Step 6: Commit**
```bash
git add SportsTime/SportsTime/SportsTimeApp.swift
git commit -m "feat(iap): integrate StoreManager in app lifecycle with onboarding paywall"
```
---
## Task 10: Gate Trip Saving
**Files:**
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift:525-540`
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift`
**Step 1: Add saved trip count query**
@@ -1468,25 +1351,12 @@ Add sheet modifier to the view body (after existing sheets around line 93):
}
```
**Step 5: Run tests**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: PASS
**Step 6: Commit**
```bash
git add SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift
git commit -m "feat(iap): gate trip saving to 1 trip for free users"
```
---
## Task 11: Gate PDF Export
**Files:**
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift:72-78`
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift`
**Step 1: Modify export button in toolbar**
@@ -1512,25 +1382,12 @@ Replace the PDF export button (around line 72) with gated version:
}
```
**Step 2: Run tests**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: PASS
**Step 3: Commit**
```bash
git add SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift
git commit -m "feat(iap): gate PDF export behind Pro subscription"
```
---
## Task 12: Gate Progress Tracking Tab
**Files:**
- Modify: `SportsTime/SportsTime/Features/Home/Views/HomeView.swift:86-95`
- Modify: `SportsTime/SportsTime/Features/Home/Views/HomeView.swift`
**Step 1: Add paywall state to HomeView**
@@ -1634,19 +1491,6 @@ Add after the existing sheet modifiers (around line 123):
}
```
**Step 5: Run tests**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: PASS
**Step 6: Commit**
```bash
git add SportsTime/SportsTime/Features/Home/Views/HomeView.swift
git commit -m "feat(iap): gate Progress tab behind Pro subscription"
```
---
## Task 13: Add Subscription Management to Settings
@@ -1663,15 +1507,21 @@ Add new section after `aboutSection` (around line 173):
subscriptionSection
```
**Step 2: Implement subscription section**
**Step 2: Add showPaywall state**
Add at top of SettingsView with other @State properties:
```swift
@State private var showPaywall = false
```
**Step 3: Implement subscription section**
Add the section implementation:
```swift
// MARK: - Subscription Section
@State private var showPaywall = false
private var subscriptionSection: some View {
Section {
if StoreManager.shared.isPro {
@@ -1748,32 +1598,27 @@ Add the section implementation:
}
```
**Step 3: Run tests**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: PASS
**Step 4: Commit**
```bash
git add SportsTime/SportsTime/Features/Settings/Views/SettingsView.swift
git commit -m "feat(iap): add subscription management to Settings"
```
---
## Task 14: Run Full Test Suite and Manual Verification
## Task 14: Build, Test, and Fix Issues
**Step 1: Run all tests**
**Step 1: Build the project**
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```
Fix any compilation errors that arise.
**Step 2: Run all tests**
```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
```
Expected: All tests PASS
Fix any test failures.
**Step 2: Manual testing checklist**
**Step 3: Manual testing checklist**
Test in Simulator with StoreKit configuration:
@@ -1794,7 +1639,7 @@ Test in Simulator with StoreKit configuration:
- [ ] Settings shows "Manage Subscription" for Pro users
- [ ] Restore Purchases works
**Step 3: Commit final state**
**Step 4: Commit all changes**
```bash
git add -A