UI overhaul: new color palette, trip creation improvements, crash fix
Theme: - New teal/cyan/mint/pink/gold color palette replacing orange/cream - Added Theme.swift, ViewModifiers.swift, AnimatedComponents.swift Trip Creation: - Removed Drive/Fly toggle (drive-only for now) - Removed Lodging Type picker - Renamed "Number of Stops" to "Number of Cities" with explanation - Added explanation for "Find Other Sports Along Route" - Removed staggered animation from trip options list Bug Fix: - Disabled AI route description generation (Foundation Models crashes in iOS 26.2 Simulator due to NLLanguageRecognizer assertion failure) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import SwiftData
|
||||
|
||||
struct HomeView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Query(sort: \SavedTrip.updatedAt, order: .reverse) private var savedTrips: [SavedTrip]
|
||||
|
||||
@State private var showNewTrip = false
|
||||
@@ -18,30 +19,37 @@ struct HomeView: View {
|
||||
// Home Tab
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
VStack(spacing: Theme.Spacing.xl) {
|
||||
// Hero Card
|
||||
heroCard
|
||||
.staggeredAnimation(index: 0)
|
||||
|
||||
// Quick Actions
|
||||
quickActions
|
||||
.staggeredAnimation(index: 1)
|
||||
|
||||
// Saved Trips
|
||||
if !savedTrips.isEmpty {
|
||||
savedTripsSection
|
||||
.staggeredAnimation(index: 2)
|
||||
}
|
||||
|
||||
// Featured / Tips
|
||||
tipsSection
|
||||
.staggeredAnimation(index: 3)
|
||||
}
|
||||
.padding()
|
||||
.padding(Theme.Spacing.md)
|
||||
}
|
||||
.navigationTitle("Sport Travel Planner")
|
||||
.themedBackground()
|
||||
.navigationTitle("SportsTime")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +86,7 @@ struct HomeView: View {
|
||||
}
|
||||
.tag(3)
|
||||
}
|
||||
.tint(Theme.warmOrange)
|
||||
.sheet(isPresented: $showNewTrip) {
|
||||
TripCreationView()
|
||||
}
|
||||
@@ -86,49 +95,57 @@ struct HomeView: View {
|
||||
// MARK: - Hero Card
|
||||
|
||||
private var heroCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Plan Your Ultimate Sports Road Trip")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Adventure Awaits")
|
||||
.font(.system(size: Theme.FontSize.heroTitle, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text("Visit multiple stadiums, catch live games, and create unforgettable memories.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Plan your ultimate sports road trip. Visit stadiums, catch games, and create unforgettable memories.")
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
Text("Start Planning")
|
||||
.fontWeight(.semibold)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: "map.fill")
|
||||
Text("Start Planning")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.warmOrange)
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
.pressableStyle()
|
||||
.glowEffect(color: Theme.warmOrange, radius: 12)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [.blue.opacity(0.1), .green.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.padding(Theme.Spacing.lg)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8)
|
||||
}
|
||||
|
||||
// MARK: - Quick Actions
|
||||
|
||||
private var quickActions: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Quick Start")
|
||||
.font(.headline)
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
ForEach(Sport.supported) { sport in
|
||||
QuickSportButton(sport: sport) {
|
||||
// Start trip with this sport pre-selected
|
||||
showNewTrip = true
|
||||
}
|
||||
}
|
||||
@@ -139,20 +156,29 @@ struct HomeView: View {
|
||||
// MARK: - Saved Trips
|
||||
|
||||
private var savedTripsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
HStack {
|
||||
Text("Recent Trips")
|
||||
.font(.headline)
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Spacer()
|
||||
Button("See All") {
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("See All")
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
ForEach(savedTrips.prefix(3)) { savedTrip in
|
||||
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
SavedTripCard(trip: trip)
|
||||
SavedTripCard(savedTrip: savedTrip, trip: trip)
|
||||
.staggeredAnimation(index: index, delay: 0.05)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,18 +187,23 @@ struct HomeView: View {
|
||||
// MARK: - Tips
|
||||
|
||||
private var tipsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Planning Tips")
|
||||
.font(.headline)
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
TipRow(icon: "calendar.badge.clock", title: "Check schedules early", subtitle: "Game times can change, sync often")
|
||||
TipRow(icon: "car.fill", title: "Plan rest days", subtitle: "Don't overdo the driving")
|
||||
TipRow(icon: "star.fill", title: "Mark must-sees", subtitle: "Ensure your favorite matchups are included")
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,58 +213,107 @@ struct HomeView: View {
|
||||
struct QuickSportButton: View {
|
||||
let sport: Sport
|
||||
let action: () -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var isPressed = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title)
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(sport.themeColor.opacity(0.15))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title2)
|
||||
.foregroundStyle(sport.themeColor)
|
||||
}
|
||||
|
||||
Text(sport.rawValue)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .medium))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.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)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = true }
|
||||
}
|
||||
.onEnded { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = false }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SavedTripCard: View {
|
||||
let savedTrip: SavedTrip
|
||||
let trip: Trip
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: [:])
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
HStack {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Route preview icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Theme.warmOrange.opacity(0.15))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: "map.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Label("\(trip.stops.count) cities", systemImage: "mappin")
|
||||
Label("\(trip.totalGames) games", systemImage: "sportscourt")
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin")
|
||||
.font(.caption2)
|
||||
Text("\(trip.stops.count) cities")
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sportscourt")
|
||||
.font(.caption2)
|
||||
Text("\(trip.totalGames) games")
|
||||
}
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -243,21 +323,27 @@ struct TipRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.title3)
|
||||
.foregroundStyle(.blue)
|
||||
.frame(width: 30)
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Theme.routeGold.opacity(0.15))
|
||||
.frame(width: 36, height: 36)
|
||||
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -269,37 +355,100 @@ struct TipRow: View {
|
||||
|
||||
struct SavedTripsListView: View {
|
||||
let trips: [SavedTrip]
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Group {
|
||||
if trips.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Saved Trips",
|
||||
systemImage: "suitcase",
|
||||
description: Text("Your planned trips will appear here")
|
||||
EmptyStateView(
|
||||
icon: "suitcase",
|
||||
title: "No Saved Trips",
|
||||
message: "Your planned adventures will appear here. Start planning your first sports road trip!"
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ForEach(trips) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: [:])
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
Text(trip.name)
|
||||
.font(.headline)
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
ScrollView {
|
||||
LazyVStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(Array(trips.enumerated()), id: \.element.id) { index, savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
SavedTripListRow(trip: trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.staggeredAnimation(index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
}
|
||||
}
|
||||
}
|
||||
.themedBackground()
|
||||
.navigationTitle("My Trips")
|
||||
}
|
||||
}
|
||||
|
||||
struct SavedTripListRow: View {
|
||||
let trip: Trip
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Route preview
|
||||
VStack(spacing: 4) {
|
||||
ForEach(0..<min(3, trip.stops.count), id: \.self) { i in
|
||||
Circle()
|
||||
.fill(Theme.warmOrange.opacity(Double(3 - i) / 3))
|
||||
.frame(width: 8, height: 8)
|
||||
if i < min(2, trip.stops.count - 1) {
|
||||
Rectangle()
|
||||
.fill(Theme.routeGold.opacity(0.5))
|
||||
.frame(width: 2, height: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Route preview strip
|
||||
if !trip.stops.isEmpty {
|
||||
RoutePreviewStrip(cities: trip.stops.map { $0.city })
|
||||
}
|
||||
|
||||
// Stats
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
StatPill(icon: "mappin.circle", value: "\(trip.stops.count) cities")
|
||||
StatPill(icon: "sportscourt", value: "\(trip.totalGames) games")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.padding(Theme.Spacing.lg)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: Theme.cardShadow(colorScheme), radius: 8, y: 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HomeView()
|
||||
.modelContainer(for: SavedTrip.self, inMemory: true)
|
||||
|
||||
Reference in New Issue
Block a user