New 4th planning mode for fans to build trips around their team's schedule (home + away games). Includes region/date filtering, flexible start/end location, and repeat city handling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.3 KiB
8.3 KiB
Follow Team Mode Design
Overview
Add a 4th trip planning mode called "Follow Team" where users select a team, regions, and date range to build trips around that team's schedule (both home and away games).
User Flow
- Select "Follow Team" mode
- Pick a team (grouped by sport for browsing)
- Select regions (East/Central/West)
- Pick date range
- Choose whether to start/end from home location or fly-in/fly-out
- Plan trip
Scope
Included:
- Single team selection
- Both home and away games
- Region filtering (same as existing modes)
- Date range filtering
- Flexible start/end: home location OR first-game-to-last-game
- Respect
allowRepeatCitiestoggle
Not included (future):
- Multiple teams (rivalry trips)
- Opponent filtering
- Win/loss context display
Data Model Changes
PlanningMode Extension
enum PlanningMode: String, Codable, CaseIterable, Identifiable {
case dateRange // Scenario A - dates first
case gameFirst // Scenario B - pick games
case locations // Scenario C - start/end cities
case followTeam // Scenario D - follow one team
var displayName: String {
switch self {
case .dateRange: return "By Dates"
case .gameFirst: return "By Games"
case .locations: return "By Route"
case .followTeam: return "Follow Team"
}
}
var description: String {
switch self {
case .dateRange: return "Shows a curated sample of possible routes"
case .gameFirst: return "Build trip around specific games"
case .locations: return "Plan route between locations"
case .followTeam: return "Follow your team on the road"
}
}
var iconName: String {
switch self {
case .dateRange: return "calendar"
case .gameFirst: return "sportscourt"
case .locations: return "map"
case .followTeam: return "person.3.fill"
}
}
}
TripPreferences Addition
struct TripPreferences {
// ... existing properties ...
/// Team to follow (for Follow Team mode)
var followTeamId: UUID?
/// Whether to start/end from a home location (vs fly-in/fly-out)
var useHomeLocation: Bool = true
}
PlanningScenario Extension
enum PlanningScenario: Equatable {
case scenarioA // Date range
case scenarioB // Selected games
case scenarioC // Start/end locations
case scenarioD // Follow team
}
ScenarioDPlanner Logic
Core Algorithm
actor ScenarioDPlanner {
func plan(request: PlanningRequest) async throws -> ItineraryResult {
// 1. Filter games to selected team only
let teamGames = filterToTeam(
request.allGames,
teamId: request.preferences.followTeamId
)
// 2. Apply region filter
let regionalGames = filterByRegion(
teamGames,
regions: request.preferences.selectedRegions,
stadiums: request.stadiums
)
// 3. Apply date range
let dateFilteredGames = filterByDateRange(
regionalGames,
start: request.preferences.startDate,
end: request.preferences.endDate
)
// 4. Apply repeat city constraint
let finalGames = applyRepeatCityFilter(
dateFilteredGames,
allowRepeat: request.preferences.allowRepeatCities,
stadiums: request.stadiums
)
// 5. Build route (with or without home location)
if request.preferences.useHomeLocation,
let startLocation = request.startLocation {
// Round-trip from home
return buildRouteFromHome(finalGames, start: startLocation)
} else {
// Fly-in / fly-out (first game to last game)
return buildPointToPointRoute(finalGames)
}
}
}
Team Game Filtering
A game belongs to the followed team if they're home OR away:
func filterToTeam(_ games: [Game], teamId: UUID?) -> [Game] {
guard let teamId else { return [] }
return games.filter { game in
game.homeTeamId == teamId || game.awayTeamId == teamId
}
}
Repeat City Handling
When allowRepeatCities = false, keep only one game per city:
func applyRepeatCityFilter(_ games: [Game], allowRepeat: Bool, stadiums: [UUID: Stadium]) -> [Game] {
guard !allowRepeat else { return games }
var seenCities: Set<String> = []
return games.filter { game in
guard let stadium = stadiums[game.stadiumId] else { return false }
if seenCities.contains(stadium.city) { return false }
seenCities.insert(stadium.city)
return true
}
}
UI Changes
Mode Selector
Expand to 4 modes in a 2x2 grid:
var planningModeSection: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
ForEach(PlanningMode.allCases) { mode in
PlanningModeCard(
mode: mode,
isSelected: viewModel.planningMode == mode,
action: { viewModel.planningMode = mode }
)
}
}
}
Follow Team Mode Sections
case .followTeam:
teamPickerSection // Select team (grouped by sport)
regionSection // East/Central/West
datesSection // Start/end dates
homeLocationToggle // "Start from home?" toggle
locationSection // Conditional: Only if toggle is on
Team Picker Section
var teamPickerSection: some View {
Section("Select Team") {
Picker("Team", selection: $viewModel.followTeamId) {
Text("Choose a team").tag(nil as UUID?)
ForEach(teamsByLeague) { league in
Section(league.name) {
ForEach(league.teams) { team in
Text(team.name).tag(team.id as UUID?)
}
}
}
}
}
}
Home Location Toggle
var homeLocationToggle: some View {
Toggle("Start and end from home", isOn: $viewModel.useHomeLocation)
.toggleStyle(.switch)
}
Validation Rules
var followTeamValidation: String? {
guard viewModel.followTeamId != nil else {
return "Select a team to follow"
}
guard viewModel.endDate > viewModel.startDate else {
return "End date must be after start date"
}
if viewModel.useHomeLocation && viewModel.startLocation == nil {
return "Enter your home location"
}
return nil
}
Edge Cases
| Case | Behavior |
|---|---|
| No games in range | Show: "No [Team] games found in [Region] between [dates]" |
| All games in one city | Valid single-stop trip |
| Repeat city + toggle off | Keep first game in each city, skip duplicates |
| Back-to-back different cities | Existing driving constraints handle feasibility |
| No start location but toggle on | Block submission with validation message |
Planning Failures
Reuse existing PlanningFailure reasons:
.noGamesInRange— No team games in date/region.noValidRoutes— Can't build feasible route.repeatCityViolation— Would require repeat city but toggle is off
Implementation Plan
Files to Create
SportsTime/Planning/Engine/ScenarioDPlanner.swift
SportsTimeTests/Planning/ScenarioDPlannerTests.swift
Files to Modify
SportsTime/Core/Models/Domain/TripPreferences.swift
SportsTime/Planning/Models/PlanningModels.swift
SportsTime/Planning/Engine/TripPlanningEngine.swift
SportsTime/Features/Trip/Views/TripCreationView.swift
SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift
Implementation Order
- Data model — Add properties to
TripPreferences, addscenarioD - Planner — Create
ScenarioDPlanner - Engine routing — Update
TripPlanningEngineto route to Scenario D - ViewModel — Add state and validation
- UI — Add team picker, toggle, mode card
- Tests — Cover all edge cases
Test Cases
- Happy path: Team with 5 games in region → valid route
- No games: Team has no games in range → proper failure
- Single city: All home games, repeat allowed → valid single-stop
- Repeat city blocked: Multiple home games, repeat off → keeps one
- With home location: Round-trip from user's city
- Without home location: Point-to-point first to last game