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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -689,7 +689,7 @@ struct GameRow: View {
|
||||
Spacer()
|
||||
|
||||
// Time
|
||||
Text(game.game.gameTime)
|
||||
Text(game.localGameTimeShort)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user