Remove CFB/NASCAR/PGA and streamline to 8 supported sports

- Remove College Football, NASCAR, and PGA from scraper and app
- Clean all data files (stadiums, games, pipeline reports)
- Update Sport.swift enum and all UI components
- Add sportstime.py CLI tool for pipeline management
- Add DATA_SCRAPING.md documentation
- Add WNBA/MLS/NWSL implementation documentation
- Scraper now supports: NBA, MLB, NHL, NFL, WNBA, MLS, NWSL, CBB

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-09 23:22:13 -06:00
parent f5e509a9ae
commit 8790d2ad73
35 changed files with 117819 additions and 65871 deletions

View File

@@ -169,19 +169,40 @@ struct HomeView: View {
// MARK: - Quick Actions
private var quickActions: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack(spacing: Theme.Spacing.sm) {
ForEach(Sport.supported) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
VStack(spacing: Theme.Spacing.md) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: Theme.Spacing.sm) {
ForEach(row) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
}
}
// Fill remaining space if row has fewer than 4 items
if row.count < 4 {
ForEach(0..<(4 - row.count), id: \.self) { _ in
Color.clear.frame(maxWidth: .infinity)
}
}
}
}
}
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, 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)
}
}
}
@@ -339,30 +360,23 @@ struct QuickSportButton: View {
var body: some View {
Button(action: action) {
VStack(spacing: Theme.Spacing.xs) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(sport.themeColor.opacity(0.15))
.frame(width: 44, height: 44)
.frame(width: 48, height: 48)
Image(systemName: sport.iconName)
.font(.title2)
.font(.system(size: 20))
.foregroundStyle(sport.themeColor)
}
Text(sport.rawValue)
.font(.system(size: Theme.FontSize.micro, weight: .medium))
.font(.system(size: 10, weight: .medium))
.foregroundStyle(Theme.textSecondary(colorScheme))
}
.frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.sm)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.scaleEffect(isPressed ? 0.95 : 1.0)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
.buttonStyle(.plain)
.simultaneousGesture(

View File

@@ -244,13 +244,7 @@ struct SettingsView: View {
// MARK: - Helpers
private func sportColor(for sport: Sport) -> Color {
switch sport {
case .mlb: return .red
case .nba: return .orange
case .nhl: return .blue
case .nfl: return .green
case .mls: return .purple
}
sport.themeColor
}
}

View File

@@ -58,8 +58,24 @@ final class TripCreationViewModel {
}
// Dates
var startDate: Date = Date()
var endDate: Date = Date().addingTimeInterval(86400 * 7)
var startDate: Date = Date() {
didSet {
// Clear cached games when start date changes
if !Calendar.current.isDate(startDate, inSameDayAs: oldValue) {
availableGames = []
games = []
}
}
}
var endDate: Date = Date().addingTimeInterval(86400 * 7) {
didSet {
// Clear cached games when end date changes
if !Calendar.current.isDate(endDate, inSameDayAs: oldValue) {
availableGames = []
games = []
}
}
}
// Trip duration for game-first mode (days before/after selected games)
var tripBufferDays: Int = 2

View File

@@ -524,22 +524,36 @@ struct TripCreationView: View {
}
private var sportsSection: some View {
ThemedSection(title: "Sports") {
HStack(spacing: Theme.Spacing.sm) {
ForEach(Sport.supported) { sport in
SportSelectionChip(
sport: sport,
isSelected: viewModel.selectedSports.contains(sport),
onTap: {
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return ThemedSection(title: "Sports") {
VStack(spacing: Theme.Spacing.sm) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: Theme.Spacing.sm) {
ForEach(row) { sport in
SportSelectionChip(
sport: sport,
isSelected: viewModel.selectedSports.contains(sport),
onTap: {
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
}
}
)
}
// Fill remaining space if row has fewer than 4 items
if row.count < 4 {
ForEach(0..<(4 - row.count), id: \.self) { _ in
Color.clear.frame(maxWidth: .infinity)
}
}
)
}
}
}
.padding(.vertical, Theme.Spacing.xs)
}
}
@@ -2139,27 +2153,45 @@ struct SportSelectionChip: View {
let isSelected: Bool
let onTap: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var isPressed = false
var body: some View {
Button(action: onTap) {
VStack(spacing: Theme.Spacing.xs) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(isSelected ? sport.themeColor : sport.themeColor.opacity(0.15))
.frame(width: 44, height: 44)
.frame(width: 48, height: 48)
.overlay {
if isSelected {
Circle()
.stroke(sport.themeColor.opacity(0.3), lineWidth: 3)
.frame(width: 54, height: 54)
}
}
Image(systemName: sport.iconName)
.font(.title3)
.font(.system(size: 20))
.foregroundStyle(isSelected ? .white : sport.themeColor)
}
Text(sport.rawValue)
.font(.system(size: Theme.FontSize.micro, weight: .medium))
.font(.system(size: 10, weight: isSelected ? .semibold : .medium))
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : Theme.textSecondary(colorScheme))
}
.frame(maxWidth: .infinity)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
.buttonStyle(.plain)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(Theme.Animation.spring) { isPressed = true }
}
.onEnded { _ in
withAnimation(Theme.Animation.spring) { isPressed = false }
}
)
}
}