Improve UI consistency and add heart save button

- Add themed background to all tab views (Schedule, Settings, My Trips)
- Remove navigation titles from all screens (tab bar provides context)
- Add empty state to My Trips view
- Remove max driving hours slider from trip planner (available in Settings)
- Replace save menu with heart button overlay on trip detail map
- Add ability to unsave trips by tapping heart again

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-08 11:05:28 -06:00
parent 415202e7f4
commit 8bf8bb49cb
5 changed files with 86 additions and 67 deletions

View File

@@ -48,7 +48,6 @@ struct HomeView: View {
.padding(Theme.Spacing.md) .padding(Theme.Spacing.md)
} }
.themedBackground() .themedBackground()
.navigationTitle("SportsTime")
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button { Button {
@@ -470,16 +469,28 @@ struct SavedTripsListView: View {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
var body: some View { var body: some View {
Group {
if trips.isEmpty {
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 {
ScrollView { ScrollView {
if trips.isEmpty {
VStack(spacing: 16) {
Spacer()
.frame(height: 100)
Image(systemName: "suitcase")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("No Saved Trips")
.font(.title2)
.fontWeight(.semibold)
Text("Browse featured trips on the Home tab or create your own to get started.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
}
.frame(maxWidth: .infinity)
} else {
LazyVStack(spacing: Theme.Spacing.md) { LazyVStack(spacing: Theme.Spacing.md) {
ForEach(Array(trips.enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(trips.enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
@@ -496,9 +507,7 @@ struct SavedTripsListView: View {
.padding(Theme.Spacing.md) .padding(Theme.Spacing.md)
} }
} }
}
.themedBackground() .themedBackground()
.navigationTitle("My Trips")
} }
} }

View File

@@ -21,8 +21,9 @@ struct ScheduleListView: View {
gamesList gamesList
} }
} }
.navigationTitle("Schedule")
.searchable(text: $viewModel.searchText, prompt: "Search teams or venues") .searchable(text: $viewModel.searchText, prompt: "Search teams or venues")
.scrollContentBackground(.hidden)
.themedBackground()
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Menu { Menu {

View File

@@ -29,7 +29,8 @@ struct SettingsView: View {
// Reset // Reset
resetSection resetSection
} }
.navigationTitle("Settings") .scrollContentBackground(.hidden)
.themedBackground()
.alert("Reset Settings", isPresented: $showResetConfirmation) { .alert("Reset Settings", isPresented: $showResetConfirmation) {
Button("Cancel", role: .cancel) { } Button("Cancel", role: .cancel) { }
Button("Reset", role: .destructive) { Button("Reset", role: .destructive) {

View File

@@ -82,7 +82,6 @@ struct TripCreationView: View {
.padding(Theme.Spacing.md) .padding(Theme.Spacing.md)
} }
.themedBackground() .themedBackground()
.navigationTitle("Plan Your Trip")
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { Button("Cancel") {
@@ -551,21 +550,6 @@ struct TripCreationView: View {
onIncrement: { viewModel.numberOfDrivers += 1 }, onIncrement: { viewModel.numberOfDrivers += 1 },
onDecrement: { viewModel.numberOfDrivers -= 1 } onDecrement: { viewModel.numberOfDrivers -= 1 }
) )
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
HStack {
Text("Max Hours/Driver/Day")
.font(.system(size: Theme.FontSize.caption))
.foregroundStyle(Theme.textSecondary(colorScheme))
Spacer()
Text("\(Int(viewModel.maxDrivingHoursPerDriver))h")
.font(.system(size: Theme.FontSize.caption, weight: .bold))
.foregroundStyle(Theme.warmOrange)
}
Slider(value: $viewModel.maxDrivingHoursPerDriver, in: 4...12, step: 1)
.tint(Theme.warmOrange)
}
} }
} }
} }
@@ -1132,8 +1116,6 @@ struct TripOptionsView: View {
.padding(.bottom, Theme.Spacing.xxl) .padding(.bottom, Theme.Spacing.xxl)
} }
.themedBackground() .themedBackground()
.navigationTitle("Choose Your Trip")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(isPresented: $showTripDetail) { .navigationDestination(isPresented: $showTripDetail) {
if let trip = selectedTrip { if let trip = selectedTrip {
TripDetailView(trip: trip, games: games) TripDetailView(trip: trip, games: games)

View File

@@ -21,7 +21,6 @@ struct TripDetailView: View {
@State private var shareURL: URL? @State private var shareURL: URL?
@State private var mapCameraPosition: MapCameraPosition = .automatic @State private var mapCameraPosition: MapCameraPosition = .automatic
@State private var isSaved = false @State private var isSaved = false
@State private var showSaveConfirmation = false
@State private var routePolylines: [MKPolyline] = [] @State private var routePolylines: [MKPolyline] = []
@State private var isLoadingRoutes = false @State private var isLoadingRoutes = false
@@ -57,8 +56,6 @@ struct TripDetailView: View {
} }
} }
.background(Theme.backgroundGradient(colorScheme)) .background(Theme.backgroundGradient(colorScheme))
.navigationTitle(trip.name)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(Theme.cardBackground(colorScheme), for: .navigationBar) .toolbarBackground(Theme.cardBackground(colorScheme), for: .navigationBar)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .primaryAction) { ToolbarItemGroup(placement: .primaryAction) {
@@ -71,23 +68,12 @@ struct TripDetailView: View {
.foregroundStyle(Theme.warmOrange) .foregroundStyle(Theme.warmOrange)
} }
Menu {
Button { Button {
Task { Task {
await exportPDF() await exportPDF()
} }
} label: { } label: {
Label("Export PDF", systemImage: "doc.fill") Image(systemName: "doc.fill")
}
Button {
saveTrip()
} label: {
Label(isSaved ? "Saved" : "Save Trip", systemImage: isSaved ? "bookmark.fill" : "bookmark")
}
.disabled(isSaved)
} label: {
Image(systemName: "ellipsis.circle")
.foregroundStyle(Theme.warmOrange) .foregroundStyle(Theme.warmOrange)
} }
} }
@@ -104,11 +90,6 @@ struct TripDetailView: View {
ShareSheet(items: [trip.name, trip.formattedDateRange]) ShareSheet(items: [trip.name, trip.formattedDateRange])
} }
} }
.alert("Trip Saved", isPresented: $showSaveConfirmation) {
Button("OK", role: .cancel) { }
} message: {
Text("Your trip has been saved and can be accessed from My Trips.")
}
.onAppear { .onAppear {
checkIfSaved() checkIfSaved()
} }
@@ -132,6 +113,22 @@ struct TripDetailView: View {
} }
} }
.mapStyle(colorScheme == .dark ? .standard(elevation: .flat, emphasis: .muted) : .standard) .mapStyle(colorScheme == .dark ? .standard(elevation: .flat, emphasis: .muted) : .standard)
.overlay(alignment: .topTrailing) {
// Save/Unsave heart button
Button {
toggleSaved()
} label: {
Image(systemName: isSaved ? "heart.fill" : "heart")
.font(.system(size: 22, weight: .medium))
.foregroundStyle(isSaved ? .red : .white)
.padding(12)
.background(.ultraThinMaterial)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4, y: 2)
}
.padding(.top, 12)
.padding(.trailing, 12)
}
// Gradient overlay at bottom // Gradient overlay at bottom
LinearGradient( LinearGradient(
@@ -449,6 +446,14 @@ struct TripDetailView: View {
showShareSheet = true showShareSheet = true
} }
private func toggleSaved() {
if isSaved {
unsaveTrip()
} else {
saveTrip()
}
}
private func saveTrip() { private func saveTrip() {
guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else { guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else {
print("Failed to create SavedTrip") print("Failed to create SavedTrip")
@@ -459,13 +464,34 @@ struct TripDetailView: View {
do { do {
try modelContext.save() try modelContext.save()
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = true isSaved = true
showSaveConfirmation = true }
} catch { } catch {
print("Failed to save trip: \(error)") print("Failed to save trip: \(error)")
} }
} }
private func unsaveTrip() {
let tripId = trip.id
let descriptor = FetchDescriptor<SavedTrip>(
predicate: #Predicate { $0.id == tripId }
)
do {
let savedTrips = try modelContext.fetch(descriptor)
for savedTrip in savedTrips {
modelContext.delete(savedTrip)
}
try modelContext.save()
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = false
}
} catch {
print("Failed to unsave trip: \(error)")
}
}
private func checkIfSaved() { private func checkIfSaved() {
let tripId = trip.id let tripId = trip.id
let descriptor = FetchDescriptor<SavedTrip>( let descriptor = FetchDescriptor<SavedTrip>(