fix: multiple bug fixes and improvements

- Fix suggested trips showing wrong sports for cross-country trips
- Remove quick start sections from home variants (Classic, Spotify)
- Remove dead quickActions code from HomeView
- Fix pace capsule animation in TripCreationView
- Add text wrapping to achievement descriptions
- Improve poll parsing with better error handling
- Various sharing system improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-14 09:35:18 -06:00
parent fe36f99bca
commit d034ee8612
22 changed files with 422 additions and 242 deletions

View File

@@ -157,29 +157,6 @@ struct HomeView: View {
.shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8)
}
// MARK: - Quick Actions
private var quickActions: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
SportSelectorGrid { sport in
selectedSport = sport
showNewTrip = true
}
.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)
}
}
}
// MARK: - Suggested Trips
@ViewBuilder

View File

@@ -28,10 +28,6 @@ struct HomeContent_Classic: View {
.padding(.horizontal, Theme.Spacing.md)
.padding(.top, Theme.Spacing.sm)
// Quick Actions
quickActions
.padding(.horizontal, Theme.Spacing.md)
// Suggested Trips
suggestedTripsSection
.padding(.horizontal, Theme.Spacing.md)
@@ -97,28 +93,6 @@ struct HomeContent_Classic: View {
.shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8)
}
// MARK: - Quick Actions
private var quickActions: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
SportSelectorGrid { _ in
showNewTrip = true
}
.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)
}
}
}
// MARK: - Suggested Trips
@ViewBuilder

View File

@@ -36,10 +36,6 @@ struct HomeContent_Spotify: View {
.padding(.horizontal, 16)
.padding(.top, 12)
// Quick actions
quickActions
.padding(.horizontal, 16)
// Your trips
if !savedTrips.isEmpty {
yourTripsSection
@@ -91,62 +87,6 @@ struct HomeContent_Spotify: View {
}
}
// MARK: - Quick Actions
private var quickActions: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
// New trip button
quickActionButton(title: "Plan Trip", icon: "plus.circle.fill", isPrimary: true) {
showNewTrip = true
}
// Saved trips button
if !savedTrips.isEmpty {
quickActionButton(title: "Your Trips", icon: "folder.fill", isPrimary: false) {
selectedTab = 2
}
}
// First saved trip quick access
if let firstTrip = savedTrips.first?.trip {
quickActionButton(title: firstTrip.name, icon: "play.circle.fill", isPrimary: false) {
// Could navigate to trip
}
}
// Refresh suggestions
quickActionButton(title: "Refresh", icon: "arrow.clockwise", isPrimary: false) {
Task {
await suggestedTripsGenerator.refreshTrips()
}
}
}
}
private func quickActionButton(title: String, icon: String, isPrimary: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.system(size: 22))
.foregroundStyle(isPrimary ? spotifyGreen : textPrimary)
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(textPrimary)
.lineLimit(1)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(cardBg)
)
}
.buttonStyle(.plain)
}
// MARK: - Your Trips Section
private var yourTripsSection: some View {

View File

@@ -25,15 +25,19 @@ final class PollDetailViewModel {
return PollResults(poll: poll, votes: votes)
}
func checkIsOwner() async -> Bool {
guard let poll else { return false }
do {
let userId = try await pollService.getCurrentUserRecordID()
return poll.ownerId == userId
} catch {
return false
}
}
var isOwner: Bool {
get async {
guard let poll else { return false }
do {
let userId = try await pollService.getCurrentUserRecordID()
return poll.ownerId == userId
} catch {
return false
}
await checkIsOwner()
}
}
@@ -98,6 +102,32 @@ final class PollDetailViewModel {
isLoading = false
}
/// Load poll from an existing object (avoids CloudKit fetch delay)
func loadPoll(from existingPoll: TripPoll) async {
isLoading = true
error = nil
self.poll = existingPoll
do {
// Still fetch votes and subscription from CloudKit
async let votesTask = pollService.fetchVotes(forPollId: existingPoll.id)
async let myVoteTask = pollService.fetchMyVote(forPollId: existingPoll.id)
let (fetchedVotes, fetchedMyVote) = try await (votesTask, myVoteTask)
self.votes = fetchedVotes
self.myVote = fetchedMyVote
// Subscribe to vote updates
try? await pollService.subscribeToVoteUpdates(forPollId: existingPoll.id)
} catch {
// Votes fetch failed, but we have the poll - non-critical error
self.votes = []
}
isLoading = false
}
func refresh() async {
guard let poll else { return }

View File

@@ -18,15 +18,25 @@ struct PollDetailView: View {
let pollId: UUID?
let shareCode: String?
let initialPoll: TripPoll?
init(pollId: UUID) {
self.pollId = pollId
self.shareCode = nil
self.initialPoll = nil
}
init(shareCode: String) {
self.pollId = nil
self.shareCode = shareCode
self.initialPoll = nil
}
/// Initialize with a poll object directly (avoids fetch delay)
init(poll: TripPoll) {
self.pollId = poll.id
self.shareCode = nil
self.initialPoll = poll
}
var body: some View {
@@ -217,7 +227,10 @@ struct PollDetailView: View {
}
private func loadPoll() async {
if let pollId {
// If we have an initial poll object, use it directly to avoid CloudKit fetch delay
if let initialPoll {
await viewModel.loadPoll(from: initialPoll)
} else if let pollId {
await viewModel.loadPoll(byId: pollId)
} else if let shareCode {
await viewModel.loadPoll(byShareCode: shareCode)
@@ -291,6 +304,15 @@ private struct TripPreviewCard: View {
.font(.headline)
}
// Date range and duration
HStack {
Label(trip.formattedDateRange, systemImage: "calendar")
Spacer()
Label("\(trip.tripDuration) days", systemImage: "clock")
}
.font(.caption)
.foregroundStyle(.secondary)
HStack {
Label("\(trip.stops.count) stops", systemImage: "mappin.and.ellipse")
Spacer()
@@ -303,7 +325,7 @@ private struct TripPreviewCard: View {
Text(trip.stops.map { $0.city }.joined(separator: ""))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.lineLimit(2)
}
.padding()
.background(Theme.cardBackground(colorScheme))

View File

@@ -11,6 +11,7 @@ struct PollsListView: View {
@Environment(\.colorScheme) private var colorScheme
@State private var polls: [TripPoll] = []
@State private var isLoading = false
@State private var hasLoadedInitially = false
@State private var error: PollError?
@State private var showJoinPoll = false
@State private var joinCode = ""
@@ -39,6 +40,8 @@ struct PollsListView: View {
await loadPolls()
}
.task {
guard !hasLoadedInitially else { return }
hasLoadedInitially = true
await loadPolls()
}
.alert("Join Poll", isPresented: $showJoinPoll) {
@@ -87,7 +90,7 @@ struct PollsListView: View {
}
.listStyle(.plain)
.navigationDestination(for: TripPoll.self) { poll in
PollDetailView(pollId: poll.id)
PollDetailView(poll: poll)
}
}

View File

@@ -492,11 +492,14 @@ struct AchievementDetailSheet: View {
.font(.title2)
.fontWeight(achievement.isEarned ? .bold : .regular)
.foregroundStyle(achievement.isEarned ? completedGold : Theme.textPrimary(colorScheme))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
Text(achievement.definition.description)
.font(.body)
.foregroundStyle(Theme.textSecondary(colorScheme))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
// Category badge
HStack(spacing: 4) {

View File

@@ -269,7 +269,7 @@ struct GameRowView: View {
// Game info
HStack(spacing: 12) {
Label(game.game.gameTime, systemImage: "clock")
Label(game.localGameTimeShort, systemImage: "clock")
Label(game.stadium.name, systemImage: "building.2")
if let broadcast = game.game.broadcastInfo {

View File

@@ -1335,7 +1335,7 @@ struct GameCalendarRow: View {
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack(spacing: Theme.Spacing.xs) {
Label(game.game.gameTime, systemImage: "clock")
Label(game.localGameTimeShort, systemImage: "clock")
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
@@ -1838,9 +1838,7 @@ struct TripOptionsView: View {
Menu {
ForEach(TripPaceFilter.allCases) { pace in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
paceFilter = pace
}
paceFilter = pace
} label: {
Label(pace.rawValue, systemImage: pace.icon)
}
@@ -1865,7 +1863,6 @@ struct TripOptionsView: View {
Capsule()
.strokeBorder(paceFilter == .all ? Theme.textMuted(colorScheme).opacity(0.2) : Theme.warmOrange.opacity(0.3), lineWidth: 1)
)
.animation(nil, value: paceFilter)
}
}

View File

@@ -689,7 +689,7 @@ struct GameRow: View {
Spacer()
// Time
Text(game.game.gameTime)
Text(game.localGameTimeShort)
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}