docs: update planning documents and todos
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
52
TO-DOS.md
52
TO-DOS.md
@@ -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 doesn’t find any game
|
||||
Issue: I’m 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.
|
||||
|
||||
659
docs/plans/2026-01-12-polish-enhancements-implementation.md
Normal file
659
docs/plans/2026-01-12-polish-enhancements-implementation.md
Normal 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
799
docs/plans/2026-01-12-todo-bugs-implementation.md
Normal file
799
docs/plans/2026-01-12-todo-bugs-implementation.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user