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:
@@ -510,17 +510,49 @@ struct CKTripPoll {
|
||||
}
|
||||
|
||||
func toPoll() -> TripPoll? {
|
||||
// Required fields with explicit guards for better debugging
|
||||
guard let pollIdString = record[CKTripPoll.pollIdKey] as? String,
|
||||
let pollId = UUID(uuidString: pollIdString),
|
||||
let title = record[CKTripPoll.titleKey] as? String,
|
||||
let ownerId = record[CKTripPoll.ownerIdKey] as? String,
|
||||
let shareCode = record[CKTripPoll.shareCodeKey] as? String,
|
||||
let tripsData = record[CKTripPoll.tripSnapshotsKey] as? Data,
|
||||
let tripSnapshots = try? JSONDecoder().decode([Trip].self, from: tripsData),
|
||||
let tripVersions = record[CKTripPoll.tripVersionsKey] as? [String],
|
||||
let createdAt = record[CKTripPoll.createdAtKey] as? Date,
|
||||
let modifiedAt = record[CKTripPoll.modifiedAtKey] as? Date
|
||||
else { return nil }
|
||||
let pollId = UUID(uuidString: pollIdString)
|
||||
else {
|
||||
print("CKTripPoll.toPoll: Failed to parse pollId")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let title = record[CKTripPoll.titleKey] as? String else {
|
||||
print("CKTripPoll.toPoll: Missing title for poll \(pollId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ownerId = record[CKTripPoll.ownerIdKey] as? String else {
|
||||
print("CKTripPoll.toPoll: Missing ownerId for poll \(pollId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let shareCode = record[CKTripPoll.shareCodeKey] as? String else {
|
||||
print("CKTripPoll.toPoll: Missing shareCode for poll \(pollId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let createdAt = record[CKTripPoll.createdAtKey] as? Date else {
|
||||
print("CKTripPoll.toPoll: Missing createdAt for poll \(pollId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// modifiedAt defaults to createdAt if missing
|
||||
let modifiedAt = record[CKTripPoll.modifiedAtKey] as? Date ?? createdAt
|
||||
|
||||
// Decode trip snapshots - this is the most likely failure point
|
||||
var tripSnapshots: [Trip] = []
|
||||
if let tripsData = record[CKTripPoll.tripSnapshotsKey] as? Data {
|
||||
do {
|
||||
tripSnapshots = try JSONDecoder().decode([Trip].self, from: tripsData)
|
||||
} catch {
|
||||
print("CKTripPoll.toPoll: Failed to decode tripSnapshots for poll \(pollId): \(error)")
|
||||
// Return poll with empty trips rather than failing completely
|
||||
}
|
||||
} else {
|
||||
print("CKTripPoll.toPoll: Missing tripSnapshots data for poll \(pollId)")
|
||||
}
|
||||
|
||||
var poll = TripPoll(
|
||||
id: pollId,
|
||||
@@ -531,8 +563,13 @@ struct CKTripPoll {
|
||||
createdAt: createdAt,
|
||||
modifiedAt: modifiedAt
|
||||
)
|
||||
// Preserve the actual stored versions (not recomputed)
|
||||
poll.tripVersions = tripVersions
|
||||
|
||||
// tripVersions is optional - it's recomputed if missing
|
||||
if let storedVersions = record[CKTripPoll.tripVersionsKey] as? [String] {
|
||||
poll.tripVersions = storedVersions
|
||||
}
|
||||
// Otherwise, keep the computed versions from init
|
||||
|
||||
return poll
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,13 @@ actor PollService {
|
||||
}
|
||||
|
||||
func deletePoll(_ pollId: UUID) async throws {
|
||||
// Verify ownership before deleting
|
||||
let poll = try await fetchPoll(byId: pollId)
|
||||
let userId = try await getCurrentUserRecordID()
|
||||
guard poll.ownerId == userId else {
|
||||
throw PollError.notPollOwner
|
||||
}
|
||||
|
||||
let recordID = CKRecord.ID(recordName: pollId.uuidString)
|
||||
|
||||
do {
|
||||
@@ -193,6 +200,8 @@ actor PollService {
|
||||
try await publicDatabase.deleteRecord(withID: recordID)
|
||||
} catch let error as CKError {
|
||||
throw mapCloudKitError(error)
|
||||
} catch let error as PollError {
|
||||
throw error
|
||||
} catch {
|
||||
throw PollError.unknown(error)
|
||||
}
|
||||
|
||||
@@ -484,13 +484,17 @@ final class SuggestedTripsGenerator {
|
||||
// Build richGames dictionary
|
||||
let richGames = buildRichGames(from: selectedGames, teams: teams, stadiums: stadiums)
|
||||
|
||||
// Compute sports from games actually in the trip (not all selectedGames)
|
||||
let gameIdsInTrip = Set(trip.stops.flatMap { $0.games })
|
||||
let actualSports = Set(gameIdsInTrip.compactMap { richGames[$0]?.game.sport })
|
||||
|
||||
return SuggestedTrip(
|
||||
id: UUID(),
|
||||
region: .crossCountry,
|
||||
isSingleSport: sports.count == 1,
|
||||
isSingleSport: actualSports.count == 1,
|
||||
trip: trip,
|
||||
richGames: richGames,
|
||||
sports: sports
|
||||
sports: actualSports.isEmpty ? sports : actualSports
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -466,7 +466,7 @@ final class PDFGenerator {
|
||||
.font: UIFont.systemFont(ofSize: 11),
|
||||
.foregroundColor: UIColor.darkGray
|
||||
]
|
||||
let venueText = "\(richGame.stadium.name) | \(richGame.game.gameTime)"
|
||||
let venueText = "\(richGame.stadium.name) | \(richGame.localGameTimeShort)"
|
||||
(venueText as NSString).draw(
|
||||
at: CGPoint(x: margin + 110, y: currentY + 30),
|
||||
withAttributes: venueAttributes
|
||||
|
||||
@@ -137,6 +137,8 @@ private struct AchievementSpotlightView: View {
|
||||
Text(achievement.definition.name)
|
||||
.font(.system(size: 56, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(theme.textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// Description
|
||||
Text(achievement.definition.description)
|
||||
@@ -188,7 +190,7 @@ private struct AchievementCollectionView: View {
|
||||
|
||||
VStack(spacing: 40) {
|
||||
// Header
|
||||
Text("My \(year) Achievements")
|
||||
Text("My \(String(year)) Achievements")
|
||||
.font(.system(size: 48, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(theme.textColor)
|
||||
|
||||
@@ -271,6 +273,8 @@ private struct AchievementMilestoneView: View {
|
||||
Text(achievement.definition.name)
|
||||
.font(.system(size: 56, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(theme.textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// Description
|
||||
Text(achievement.definition.description)
|
||||
|
||||
@@ -13,7 +13,6 @@ import UIKit
|
||||
struct ProgressShareContent: ShareableContent {
|
||||
let progress: LeagueProgress
|
||||
let tripCount: Int
|
||||
let username: String?
|
||||
|
||||
var cardType: ShareCardType { .stadiumProgress }
|
||||
|
||||
@@ -29,7 +28,6 @@ struct ProgressShareContent: ShareableContent {
|
||||
let cardView = ProgressCardView(
|
||||
progress: progress,
|
||||
tripCount: tripCount,
|
||||
username: username,
|
||||
theme: theme,
|
||||
mapSnapshot: mapSnapshot
|
||||
)
|
||||
@@ -50,7 +48,6 @@ struct ProgressShareContent: ShareableContent {
|
||||
private struct ProgressCardView: View {
|
||||
let progress: LeagueProgress
|
||||
let tripCount: Int
|
||||
let username: String?
|
||||
let theme: ShareTheme
|
||||
let mapSnapshot: UIImage?
|
||||
|
||||
@@ -103,7 +100,7 @@ private struct ProgressCardView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
ShareCardFooter(theme: theme, username: username)
|
||||
ShareCardFooter(theme: theme)
|
||||
}
|
||||
.padding(ShareCardDimensions.padding)
|
||||
}
|
||||
|
||||
@@ -56,20 +56,9 @@ struct ShareCardHeader: View {
|
||||
|
||||
struct ShareCardFooter: View {
|
||||
let theme: ShareTheme
|
||||
var username: String? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
if let username = username, !username.isEmpty {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.font(.system(size: 24))
|
||||
Text("@\(username)")
|
||||
.font(.system(size: 28, weight: .medium))
|
||||
}
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.font(.system(size: 20))
|
||||
|
||||
@@ -57,8 +57,8 @@ extension ShareButton where Content == TripShareContent {
|
||||
}
|
||||
|
||||
extension ShareButton where Content == ProgressShareContent {
|
||||
init(progress: LeagueProgress, tripCount: Int = 0, username: String? = nil, style: ShareButtonStyle = .icon) {
|
||||
self.content = ProgressShareContent(progress: progress, tripCount: tripCount, username: username)
|
||||
init(progress: LeagueProgress, tripCount: Int = 0, style: ShareButtonStyle = .icon) {
|
||||
self.content = ProgressShareContent(progress: progress, tripCount: tripCount)
|
||||
self.style = style
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ struct SharePreviewView<Content: ShareableContent>: View {
|
||||
@State private var error: String?
|
||||
@State private var showCopiedToast = false
|
||||
|
||||
// Progress-specific options
|
||||
@State private var includeUsername = true
|
||||
@State private var username = ""
|
||||
|
||||
init(content: Content) {
|
||||
self.content = content
|
||||
_selectedTheme = State(initialValue: ShareThemePreferences.theme(for: content.cardType))
|
||||
@@ -38,11 +34,6 @@ struct SharePreviewView<Content: ShareableContent>: View {
|
||||
// Theme selector
|
||||
themeSelector
|
||||
|
||||
// Username toggle (progress cards only)
|
||||
if content.cardType == .stadiumProgress {
|
||||
usernameSection
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
actionButtons
|
||||
}
|
||||
@@ -158,32 +149,6 @@ struct SharePreviewView<Content: ShareableContent>: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Username Section
|
||||
|
||||
private var usernameSection: some View {
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
Toggle(isOn: $includeUsername) {
|
||||
Text("Include username")
|
||||
}
|
||||
.onChange(of: includeUsername) { _, _ in
|
||||
Task { await generatePreview() }
|
||||
}
|
||||
|
||||
if includeUsername {
|
||||
TextField("@username", text: $username)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.onChange(of: username) { _, _ in
|
||||
Task { await generatePreview() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
|
||||
// MARK: - Action Buttons
|
||||
|
||||
private var actionButtons: some View {
|
||||
@@ -204,39 +169,21 @@ struct SharePreviewView<Content: ShareableContent>: View {
|
||||
}
|
||||
.disabled(generatedImage == nil)
|
||||
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
// Copy Image
|
||||
Button {
|
||||
copyImage()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "doc.on.doc")
|
||||
Text("Copy")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
// Copy Image
|
||||
Button {
|
||||
copyImage()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "doc.on.doc")
|
||||
Text("Copy to Clipboard")
|
||||
}
|
||||
.disabled(generatedImage == nil)
|
||||
|
||||
// More Options
|
||||
Button {
|
||||
showSystemShare()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
Text("More")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
.disabled(generatedImage == nil)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
.disabled(generatedImage == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,18 +211,7 @@ struct SharePreviewView<Content: ShareableContent>: View {
|
||||
isGenerating = true
|
||||
|
||||
do {
|
||||
// For progress content, we may need to inject username
|
||||
if let progressContent = content as? ProgressShareContent {
|
||||
// This is a workaround - ideally we'd have a more elegant solution
|
||||
let modifiedContent = ProgressShareContent(
|
||||
progress: progressContent.progress,
|
||||
tripCount: progressContent.tripCount,
|
||||
username: includeUsername ? (username.isEmpty ? nil : username) : nil
|
||||
)
|
||||
generatedImage = try await modifiedContent.render(theme: selectedTheme)
|
||||
} else {
|
||||
generatedImage = try await content.render(theme: selectedTheme)
|
||||
}
|
||||
generatedImage = try await content.render(theme: selectedTheme)
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
|
||||
@@ -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