refactor: TripDetailView loads games on demand, improve poll UI

- Refactor TripDetailView to fetch games from AppDataProvider when not
  provided, adding loading state indicator for better UX
- Update all callers (25+ HomeContent variants, TripOptionsView, HomeView)
  to use simpler TripDetailView(trip:) initializer
- Fix PollDetailView sheet issue by using sheet(item:) instead of
  sheet(isPresented:) to prevent blank screen on first tap
- Improve PollDetailView UI with Theme styling, icons, and better
  visual hierarchy for share code, voting status, and results sections

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-14 11:31:05 -06:00
parent b5aea31b1a
commit 2ad458bffd
28 changed files with 314 additions and 135 deletions

View File

@@ -106,7 +106,7 @@ struct HomeView: View {
} }
.sheet(item: $selectedSuggestedTrip) { suggestedTrip in .sheet(item: $selectedSuggestedTrip) { suggestedTrip in
NavigationStack { NavigationStack {
TripDetailView(trip: suggestedTrip.trip, games: suggestedTrip.richGames) TripDetailView(trip: suggestedTrip.trip)
} }
} }
.sheet(isPresented: $showProPaywall) { .sheet(isPresented: $showProPaywall) {
@@ -315,7 +315,7 @@ struct SavedTripCard: View {
var body: some View { var body: some View {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack(spacing: Theme.Spacing.md) { HStack(spacing: Theme.Spacing.md) {
// Route preview icon // Route preview icon
@@ -555,7 +555,7 @@ struct SavedTripsListView: View {
ForEach(Array(sortedTrips.enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(sortedTrips.enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
SavedTripListRow(trip: trip) SavedTripListRow(trip: trip)
} }

View File

@@ -166,7 +166,7 @@ struct HomeContent_Airbnb: View {
ForEach(savedTrips.prefix(4)) { savedTrip in ForEach(savedTrips.prefix(4)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
tripCard(trip) tripCard(trip)
} }

View File

@@ -123,7 +123,7 @@ struct HomeContent_AppleMaps: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
recentRow(trip, isLast: index == min(2, savedTrips.count - 1)) recentRow(trip, isLast: index == min(2, savedTrips.count - 1))
} }

View File

@@ -424,7 +424,7 @@ struct HomeContent_ArtDeco: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack { HStack {
decoDiamond decoDiamond

View File

@@ -224,7 +224,7 @@ struct HomeContent_Brutalist: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack { HStack {
Text(trip.name.uppercased()) Text(trip.name.uppercased())

View File

@@ -215,7 +215,7 @@ struct HomeContent_CarrotWeather: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
tripRow(trip) tripRow(trip)
} }

View File

@@ -207,7 +207,7 @@ struct HomeContent_Classic: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
classicTripCard(savedTrip: savedTrip, trip: trip) classicTripCard(savedTrip: savedTrip, trip: trip)
} }

View File

@@ -376,7 +376,7 @@ struct HomeContent_DarkIndustrial: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack { HStack {
Rectangle() Rectangle()

View File

@@ -183,7 +183,7 @@ struct HomeContent_Fantastical: View {
ForEach(savedTrips.prefix(4)) { savedTrip in ForEach(savedTrips.prefix(4)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
eventRow(trip) eventRow(trip)
} }

View File

@@ -193,7 +193,7 @@ struct HomeContent_Flighty: View {
ForEach(savedTrips.prefix(2)) { savedTrip in ForEach(savedTrips.prefix(2)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
tripCard(trip) tripCard(trip)
} }

View File

@@ -284,7 +284,7 @@ struct HomeContent_Glassmorphism: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack(spacing: 16) { HStack(spacing: 16) {
// Glow orb // Glow orb

View File

@@ -283,7 +283,7 @@ struct HomeContent_LuxuryEditorial: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack(alignment: .top, spacing: 16) { HStack(alignment: .top, spacing: 16) {
// Number // Number

View File

@@ -392,7 +392,7 @@ struct HomeContent_MaximalistChaos: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
let colors = [skyBlue, hotOrange, electricPurple] let colors = [skyBlue, hotOrange, electricPurple]
let accentColor = colors[index % colors.count] let accentColor = colors[index % colors.count]

View File

@@ -290,7 +290,7 @@ struct HomeContent_NeoBrutalist: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack { HStack {
Rectangle() Rectangle()

View File

@@ -169,7 +169,7 @@ struct HomeContent_NikeRunClub: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
activityRow(trip, index: index) activityRow(trip, index: index)
} }

View File

@@ -248,7 +248,7 @@ struct HomeContent_Organic: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack(spacing: 14) { HStack(spacing: 14) {
// Organic dot cluster // Organic dot cluster

View File

@@ -304,7 +304,7 @@ struct HomeContent_Playful: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
let colors = [candyYellow, candyGreen, candyBlue] let colors = [candyYellow, candyGreen, candyBlue]
let accentColor = colors[index % colors.count] let accentColor = colors[index % colors.count]

View File

@@ -303,7 +303,7 @@ struct HomeContent_RetroFuturism: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
HStack { HStack {
Text(trip.name) Text(trip.name)

View File

@@ -162,7 +162,7 @@ struct HomeContent_SeatGeek: View {
ForEach(savedTrips.prefix(4)) { savedTrip in ForEach(savedTrips.prefix(4)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
yourTripCard(trip) yourTripCard(trip)
} }

View File

@@ -321,7 +321,7 @@ struct HomeContent_SoftPastel: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
let colors = [pastelPeach, pastelMint, pastelLavender] let colors = [pastelPeach, pastelMint, pastelLavender]
let accentColor = colors[index % colors.count] let accentColor = colors[index % colors.count]

View File

@@ -101,7 +101,7 @@ struct HomeContent_Spotify: View {
ForEach(savedTrips.prefix(5)) { savedTrip in ForEach(savedTrips.prefix(5)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
tripCoverCard(trip) tripCoverCard(trip)
} }

View File

@@ -210,7 +210,7 @@ struct HomeContent_Strava: View {
ForEach(savedTrips.prefix(3)) { savedTrip in ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
activityCard(trip) activityCard(trip)
} }

View File

@@ -304,7 +304,7 @@ struct HomeContent_SwissModernist: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 24) { HStack(spacing: 24) {

View File

@@ -162,7 +162,7 @@ struct HomeContent_Things3: View {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip { if let trip = savedTrip.trip {
NavigationLink { NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games) TripDetailView(trip: trip)
} label: { } label: {
tripRow(trip) tripRow(trip)
} }

View File

@@ -14,6 +14,7 @@ struct PollDetailView: View {
@State private var showShareSheet = false @State private var showShareSheet = false
@State private var showDeleteConfirmation = false @State private var showDeleteConfirmation = false
@State private var showVotingSheet = false @State private var showVotingSheet = false
@State private var selectedTrip: Trip?
@State private var isOwner = false @State private var isOwner = false
let pollId: UUID? let pollId: UUID?
@@ -123,12 +124,25 @@ struct PollDetailView: View {
} message: { } message: {
Text("This will permanently delete the poll and all votes. This action cannot be undone.") Text("This will permanently delete the poll and all votes. This action cannot be undone.")
} }
.sheet(item: $selectedTrip) { trip in
NavigationStack {
TripDetailView(trip: trip)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Done") {
selectedTrip = nil
}
}
}
}
}
} }
@ViewBuilder @ViewBuilder
private func pollContent(_ poll: TripPoll) -> some View { private func pollContent(_ poll: TripPoll) -> some View {
ScrollView { ScrollView {
VStack(spacing: 20) { VStack(spacing: Theme.Spacing.lg) {
// Share Code Card // Share Code Card
shareCodeCard(poll) shareCodeCard(poll)
@@ -143,86 +157,162 @@ struct PollDetailView: View {
// Trip Previews // Trip Previews
tripPreviewsSection(poll) tripPreviewsSection(poll)
} }
.padding() .padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.lg)
} }
.background(Theme.backgroundGradient(colorScheme))
} }
@ViewBuilder @ViewBuilder
private func shareCodeCard(_ poll: TripPoll) -> some View { private func shareCodeCard(_ poll: TripPoll) -> some View {
VStack(spacing: 8) { VStack(spacing: Theme.Spacing.md) {
// Icon
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 56, height: 56)
Image(systemName: "link.circle.fill")
.font(.system(size: 28))
.foregroundStyle(Theme.warmOrange)
}
VStack(spacing: Theme.Spacing.xs) {
Text("Share Code") Text("Share Code")
.font(.caption) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(Theme.textSecondary(colorScheme))
Text(poll.shareCode) Text(poll.shareCode)
.font(.system(size: 32, weight: .bold, design: .monospaced)) .font(.system(size: 36, weight: .bold, design: .monospaced))
.foregroundStyle(Theme.warmOrange) .foregroundStyle(Theme.warmOrange)
.tracking(4)
}
Text("sportstime://poll/\(poll.shareCode)") // Copy button
.font(.caption2) Button {
.foregroundStyle(.secondary) UIPasteboard.general.string = poll.shareCode
} label: {
Label("Copy Code", systemImage: "doc.on.doc")
.font(.subheadline.weight(.medium))
.foregroundStyle(Theme.warmOrange)
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.sm)
.background(Theme.warmOrange.opacity(0.1))
.clipShape(Capsule())
}
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding() .padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme)) .background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.strokeBorder(Theme.warmOrange.opacity(0.2), lineWidth: 1)
)
} }
@ViewBuilder @ViewBuilder
private var votingStatusCard: some View { private var votingStatusCard: some View {
HStack { HStack(spacing: Theme.Spacing.md) {
VStack(alignment: .leading, spacing: 4) { // Status icon
Text(viewModel.hasVoted ? "You voted" : "You haven't voted yet") ZStack {
.font(.headline) Circle()
.fill(viewModel.hasVoted ? Theme.mlsGreen.opacity(0.15) : Theme.warmOrange.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: viewModel.hasVoted ? "checkmark.circle.fill" : "hand.raised.fill")
.font(.title3)
.foregroundStyle(viewModel.hasVoted ? Theme.mlsGreen : Theme.warmOrange)
}
Text("\(viewModel.votes.count) total votes") VStack(alignment: .leading, spacing: 2) {
Text(viewModel.hasVoted ? "You voted" : "Cast your vote")
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "person.2.fill")
.font(.caption2)
Text("\(viewModel.votes.count) vote\(viewModel.votes.count == 1 ? "" : "s")")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) }
.foregroundStyle(Theme.textSecondary(colorScheme))
} }
Spacer() Spacer()
Button(viewModel.hasVoted ? "Change Vote" : "Vote Now") { Button {
showVotingSheet = true showVotingSheet = true
} label: {
Text(viewModel.hasVoted ? "Change" : "Vote")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white)
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.sm)
.background(Theme.warmOrange)
.clipShape(Capsule())
} }
.buttonStyle(.borderedProminent)
.tint(Theme.warmOrange)
} }
.padding() .padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme)) .background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
} }
@ViewBuilder @ViewBuilder
private func resultsSection(_ results: PollResults) -> some View { private func resultsSection(_ results: PollResults) -> some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: Theme.Spacing.md) {
// Section header
HStack(spacing: Theme.Spacing.sm) {
Image(systemName: "chart.bar.fill")
.foregroundStyle(Theme.warmOrange)
Text("Results") Text("Results")
.font(.headline) .font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
}
VStack(spacing: Theme.Spacing.sm) {
ForEach(results.tripScores, id: \.tripIndex) { item in ForEach(results.tripScores, id: \.tripIndex) { item in
let trip = results.poll.tripSnapshots[item.tripIndex] let trip = results.poll.tripSnapshots[item.tripIndex]
let rank = results.tripScores.firstIndex { $0.tripIndex == item.tripIndex }! + 1
ResultRow( ResultRow(
rank: results.tripScores.firstIndex { $0.tripIndex == item.tripIndex }! + 1, rank: rank,
tripName: trip.name, tripName: trip.name,
score: item.score, score: item.score,
percentage: results.scorePercentage(for: item.tripIndex) percentage: results.scorePercentage(for: item.tripIndex),
isLeader: rank == 1 && item.score > 0
) )
} }
} }
.padding() }
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme)) .background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
} }
@ViewBuilder @ViewBuilder
private func tripPreviewsSection(_ poll: TripPoll) -> some View { private func tripPreviewsSection(_ poll: TripPoll) -> some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: Theme.Spacing.md) {
// Section header
HStack(spacing: Theme.Spacing.sm) {
Image(systemName: "map.fill")
.foregroundStyle(Theme.warmOrange)
Text("Trip Options") Text("Trip Options")
.font(.headline) .font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Text("\(poll.tripSnapshots.count) trips")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
ForEach(Array(poll.tripSnapshots.enumerated()), id: \.element.id) { index, trip in ForEach(Array(poll.tripSnapshots.enumerated()), id: \.element.id) { index, trip in
Button {
selectedTrip = trip
} label: {
TripPreviewCard(trip: trip, index: index + 1) TripPreviewCard(trip: trip, index: index + 1)
} }
.buttonStyle(.plain)
}
} }
} }
@@ -241,43 +331,72 @@ struct PollDetailView: View {
// MARK: - Result Row // MARK: - Result Row
private struct ResultRow: View { private struct ResultRow: View {
@Environment(\.colorScheme) private var colorScheme
let rank: Int let rank: Int
let tripName: String let tripName: String
let score: Int let score: Int
let percentage: Double let percentage: Double
var isLeader: Bool = false
private var rankIcon: String {
switch rank {
case 1: return "trophy.fill"
case 2: return "medal.fill"
case 3: return "medal.fill"
default: return "\(rank).circle.fill"
}
}
private var rankColor: Color {
switch rank {
case 1: return Theme.warmOrange
case 2: return .gray
case 3: return .brown
default: return .secondary
}
}
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: Theme.Spacing.sm) {
Text("#\(rank)") // Rank badge
.font(.headline) ZStack {
.foregroundStyle(rank == 1 ? Theme.warmOrange : .secondary) Circle()
.frame(width: 30) .fill(rankColor.opacity(0.15))
.frame(width: 36, height: 36)
Image(systemName: rankIcon)
.font(.subheadline)
.foregroundStyle(rankColor)
}
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(tripName) Text(tripName)
.font(.subheadline) .font(.subheadline.weight(isLeader ? .semibold : .regular))
.foregroundStyle(Theme.textPrimary(colorScheme))
GeometryReader { geometry in GeometryReader { geometry in
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Rectangle() RoundedRectangle(cornerRadius: 4)
.fill(Color.secondary.opacity(0.2)) .fill(Theme.cardBackgroundElevated(colorScheme))
.frame(height: 8) .frame(height: 6)
.clipShape(RoundedRectangle(cornerRadius: 4))
Rectangle() RoundedRectangle(cornerRadius: 4)
.fill(rank == 1 ? Theme.warmOrange : Color.secondary) .fill(rankColor)
.frame(width: geometry.size.width * percentage, height: 8) .frame(width: max(geometry.size.width * percentage, percentage > 0 ? 6 : 0), height: 6)
.clipShape(RoundedRectangle(cornerRadius: 4))
} }
} }
.frame(height: 8) .frame(height: 6)
} }
// Score badge
Text("\(score)") Text("\(score)")
.font(.caption) .font(.subheadline.weight(.medium).monospacedDigit())
.foregroundStyle(.secondary) .foregroundStyle(rankColor)
.frame(width: 40, alignment: .trailing) .padding(.horizontal, 10)
.padding(.vertical, 4)
.background(rankColor.opacity(0.1))
.clipShape(Capsule())
} }
.padding(.vertical, Theme.Spacing.xs)
} }
} }
@@ -289,6 +408,7 @@ private struct TripPreviewCard: View {
let index: Int let index: Int
var body: some View { var body: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Text("Option \(index)") Text("Option \(index)")
@@ -302,6 +422,7 @@ private struct TripPreviewCard: View {
Text(trip.name) Text(trip.name)
.font(.headline) .font(.headline)
.foregroundStyle(.primary)
} }
// Date range and duration // Date range and duration
@@ -327,9 +448,19 @@ private struct TripPreviewCard: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2) .lineLimit(2)
} }
Image(systemName: "chevron.right")
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.tertiary)
}
.padding() .padding()
.background(Theme.cardBackground(colorScheme)) .background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.strokeBorder(Color.secondary.opacity(0.1), lineWidth: 1)
)
} }
} }

View File

@@ -12,7 +12,7 @@ struct TripDetailView: View {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
let trip: Trip let trip: Trip
let games: [String: RichGame] private let providedGames: [String: RichGame]?
@Query private var savedTrips: [SavedTrip] @Query private var savedTrips: [SavedTrip]
@State private var showProPaywall = false @State private var showProPaywall = false
@@ -25,10 +25,29 @@ struct TripDetailView: View {
@State private var isSaved = false @State private var isSaved = false
@State private var routePolylines: [MKPolyline] = [] @State private var routePolylines: [MKPolyline] = []
@State private var isLoadingRoutes = false @State private var isLoadingRoutes = false
@State private var loadedGames: [String: RichGame] = [:]
@State private var isLoadingGames = false
private let exportService = ExportService() private let exportService = ExportService()
private let dataProvider = AppDataProvider.shared private let dataProvider = AppDataProvider.shared
/// Games dictionary - uses provided games if available, otherwise uses loaded games
private var games: [String: RichGame] {
providedGames ?? loadedGames
}
/// Initialize with trip and games dictionary (existing callers)
init(trip: Trip, games: [String: RichGame]) {
self.trip = trip
self.providedGames = games
}
/// Initialize with just trip - games will be loaded from AppDataProvider
init(trip: Trip) {
self.trip = trip
self.providedGames = nil
}
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -94,6 +113,9 @@ struct TripDetailView: View {
.onAppear { .onAppear {
checkIfSaved() checkIfSaved()
} }
.task {
await loadGamesIfNeeded()
}
.overlay { .overlay {
if isExporting { if isExporting {
exportProgressOverlay exportProgressOverlay
@@ -300,6 +322,14 @@ struct TripDetailView: View {
.font(.title2) .font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme)) .foregroundStyle(Theme.textPrimary(colorScheme))
if isLoadingGames {
HStack {
Spacer()
ProgressView("Loading games...")
.padding(.vertical, Theme.Spacing.xl)
Spacer()
}
} else {
ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
switch section { switch section {
case .day(let dayNumber, let date, let gamesOnDay): case .day(let dayNumber, let date, let gamesOnDay):
@@ -316,6 +346,7 @@ struct TripDetailView: View {
} }
} }
} }
}
/// Build itinerary sections: days with travel between different cities /// Build itinerary sections: days with travel between different cities
private var itinerarySections: [ItinerarySection] { private var itinerarySections: [ItinerarySection] {
@@ -493,6 +524,34 @@ struct TripDetailView: View {
// MARK: - Actions // MARK: - Actions
/// Load games from AppDataProvider if not provided
private func loadGamesIfNeeded() async {
// Skip if games were provided
guard providedGames == nil else { return }
// Collect all game IDs from the trip
let gameIds = trip.stops.flatMap { $0.games }
guard !gameIds.isEmpty else { return }
isLoadingGames = true
// Load RichGame data from AppDataProvider
var loaded: [String: RichGame] = [:]
for gameId in gameIds {
do {
if let game = try await dataProvider.fetchGame(by: gameId),
let richGame = dataProvider.richGame(from: game) {
loaded[gameId] = richGame
}
} catch {
// Skip games that fail to load
}
}
loadedGames = loaded
isLoadingGames = false
}
private func exportPDF() async { private func exportPDF() async {
isExporting = true isExporting = true
exportProgress = nil exportProgress = nil
@@ -911,8 +970,7 @@ struct ShareSheet: UIViewControllerRepresentable {
startLocation: LocationInput(name: "New York"), startLocation: LocationInput(name: "New York"),
endLocation: LocationInput(name: "Chicago") endLocation: LocationInput(name: "Chicago")
) )
), )
games: [:]
) )
} }
} }

View File

@@ -292,7 +292,7 @@ struct TripOptionsView: View {
.themedBackground() .themedBackground()
.navigationDestination(isPresented: $showTripDetail) { .navigationDestination(isPresented: $showTripDetail) {
if let trip = selectedTrip { if let trip = selectedTrip {
TripDetailView(trip: trip, games: games) TripDetailView(trip: trip)
} }
} }
.onChange(of: showTripDetail) { _, isShowing in .onChange(of: showTripDetail) { _, isShowing in

View File

@@ -40,14 +40,4 @@ sharing needs to completely overhauled. should be able to share a trip summary,
// bugs // bugs
- fucking game show at 7 am ... the fuck? - fucking game show at 7 am ... the fuck?
all all trips view when choosing "packed" "moderate" "relaxed" the capsule the option is in does a weird animation that looks off. all all trips view when choosing "packed" "moderate" "relaxed" the capsule the option is in does a weird animation that looks off.
- Text on achievements is not wrapping and is being cutoff
- Remove username on share
- more on share doesnt do anything
- Created a poll and when I tap on it I get poll not found?
- group poll refreshed every time I go to screen, should update in bg and pull to refresh? - group poll refreshed every time I go to screen, should update in bg and pull to refresh?
- User who made it should be able to delete it
- Trip details arent showing
- Home Screen quick start should be removed
- Today games arent highlighted in the schedule tab
- features trip showing both nba and nhl but only has a nhl game