From af2a5cd204b2b5825a356e6dc63d0cd630587ed9 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 17 Jan 2026 21:24:47 -0600 Subject: [PATCH] chore: remove legacy itinerary models and services Removed: - CustomItineraryItem - TravelDayOverride - CustomItemService - CustomItemSubscriptionService - TravelOverrideService - CustomItemRow These are replaced by unified ItineraryItem model and service. Co-Authored-By: Claude Opus 4.5 --- .../Models/Domain/CustomItineraryItem.swift | 94 -------- .../Models/Domain/TravelDayOverride.swift | 38 ---- .../Core/Services/CustomItemService.swift | 207 ------------------ .../CustomItemSubscriptionService.swift | 147 ------------- .../Core/Services/TravelOverrideService.swift | 180 --------------- .../Features/Trip/Views/CustomItemRow.swift | 98 --------- 6 files changed, 764 deletions(-) delete mode 100644 SportsTime/Core/Models/Domain/CustomItineraryItem.swift delete mode 100644 SportsTime/Core/Models/Domain/TravelDayOverride.swift delete mode 100644 SportsTime/Core/Services/CustomItemService.swift delete mode 100644 SportsTime/Core/Services/CustomItemSubscriptionService.swift delete mode 100644 SportsTime/Core/Services/TravelOverrideService.swift delete mode 100644 SportsTime/Features/Trip/Views/CustomItemRow.swift diff --git a/SportsTime/Core/Models/Domain/CustomItineraryItem.swift b/SportsTime/Core/Models/Domain/CustomItineraryItem.swift deleted file mode 100644 index b49d8d5..0000000 --- a/SportsTime/Core/Models/Domain/CustomItineraryItem.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// CustomItineraryItem.swift -// SportsTime -// - -import Foundation -import CoreLocation - -struct CustomItineraryItem: Identifiable, Codable, Hashable { - let id: UUID - let tripId: UUID - var category: ItemCategory - var title: String - var day: Int // Day number (1-indexed) - var sortOrder: Double // Position within day (allows insertion between items) - let createdAt: Date - var modifiedAt: Date - - // Optional location for mappable items (from MapKit search) - var latitude: Double? - var longitude: Double? - var address: String? - - /// Whether this item has a location and can be shown on the map - var isMappable: Bool { - latitude != nil && longitude != nil - } - - /// Get coordinate if mappable - var coordinate: CLLocationCoordinate2D? { - guard let lat = latitude, let lon = longitude else { return nil } - return CLLocationCoordinate2D(latitude: lat, longitude: lon) - } - - init( - id: UUID = UUID(), - tripId: UUID, - category: ItemCategory, - title: String, - day: Int, - sortOrder: Double = 0.0, - createdAt: Date = Date(), - modifiedAt: Date = Date(), - latitude: Double? = nil, - longitude: Double? = nil, - address: String? = nil - ) { - self.id = id - self.tripId = tripId - self.category = category - self.title = title - self.day = day - self.sortOrder = sortOrder - self.createdAt = createdAt - self.modifiedAt = modifiedAt - self.latitude = latitude - self.longitude = longitude - self.address = address - } - - enum ItemCategory: String, Codable, CaseIterable { - case restaurant - case hotel - case activity - case note - - var icon: String { - switch self { - case .restaurant: return "🍽️" - case .hotel: return "🏨" - case .activity: return "🎯" - case .note: return "📝" - } - } - - var label: String { - switch self { - case .restaurant: return "Restaurant" - case .hotel: return "Hotel" - case .activity: return "Activity" - case .note: return "Note" - } - } - - var systemImage: String { - switch self { - case .restaurant: return "fork.knife" - case .hotel: return "bed.double.fill" - case .activity: return "figure.run" - case .note: return "note.text" - } - } - } -} diff --git a/SportsTime/Core/Models/Domain/TravelDayOverride.swift b/SportsTime/Core/Models/Domain/TravelDayOverride.swift deleted file mode 100644 index 0610215..0000000 --- a/SportsTime/Core/Models/Domain/TravelDayOverride.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// TravelDayOverride.swift -// SportsTime -// -// Stores user-customized travel day positions -// - -import Foundation - -/// Represents a user override of the default travel day placement. -/// Travel segments normally appear on the day after the last game in the departure city. -/// This model allows users to drag travel to a different day within valid bounds. -struct TravelDayOverride: Identifiable, Codable, Hashable { - let id: UUID - let tripId: UUID - /// Stable identifier for the travel segment (e.g., "travel:houston->arlington") - let travelAnchorId: String - /// The day number (1-indexed) where the travel should display - var displayDay: Int - let createdAt: Date - var modifiedAt: Date - - init( - id: UUID = UUID(), - tripId: UUID, - travelAnchorId: String, - displayDay: Int, - createdAt: Date = Date(), - modifiedAt: Date = Date() - ) { - self.id = id - self.tripId = tripId - self.travelAnchorId = travelAnchorId - self.displayDay = displayDay - self.createdAt = createdAt - self.modifiedAt = modifiedAt - } -} diff --git a/SportsTime/Core/Services/CustomItemService.swift b/SportsTime/Core/Services/CustomItemService.swift deleted file mode 100644 index 239157b..0000000 --- a/SportsTime/Core/Services/CustomItemService.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// CustomItemService.swift -// SportsTime -// -// CloudKit service for custom itinerary items -// - -import Foundation -import CloudKit - -actor CustomItemService { - static let shared = CustomItemService() - - private let container: CKContainer - private let publicDatabase: CKDatabase - - private init() { - self.container = CKContainer(identifier: "iCloud.com.sportstime.app") - self.publicDatabase = container.publicCloudDatabase - } - - // MARK: - CRUD Operations - - func createItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem { - print("☁️ [CloudKit] Creating record for item: \(item.id)") - let ckItem = CKCustomItineraryItem(item: item) - - do { - let savedRecord = try await publicDatabase.save(ckItem.record) - print("☁️ [CloudKit] Record saved: \(savedRecord.recordID.recordName)") - return item - } catch let error as CKError { - print("☁️ [CloudKit] CKError on create: \(error.code.rawValue) - \(error.localizedDescription)") - throw mapCloudKitError(error) - } catch { - print("☁️ [CloudKit] Unknown error on create: \(error)") - throw CustomItemError.unknown(error) - } - } - - func updateItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem { - // Fetch existing record to get changeTag - let recordID = CKRecord.ID(recordName: item.id.uuidString) - let existingRecord: CKRecord - - do { - existingRecord = try await publicDatabase.record(for: recordID) - } catch let error as CKError { - throw mapCloudKitError(error) - } catch { - throw CustomItemError.unknown(error) - } - - // Update fields - let now = Date() - existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue - existingRecord[CKCustomItineraryItem.titleKey] = item.title - existingRecord[CKCustomItineraryItem.dayKey] = item.day - existingRecord[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder - existingRecord[CKCustomItineraryItem.modifiedAtKey] = now - // Location fields (nil values clear the field in CloudKit) - existingRecord[CKCustomItineraryItem.latitudeKey] = item.latitude - existingRecord[CKCustomItineraryItem.longitudeKey] = item.longitude - existingRecord[CKCustomItineraryItem.addressKey] = item.address - - do { - try await publicDatabase.save(existingRecord) - var updatedItem = item - updatedItem.modifiedAt = now - return updatedItem - } catch let error as CKError { - throw mapCloudKitError(error) - } catch { - throw CustomItemError.unknown(error) - } - } - - /// Batch update day+sortOrder for multiple items (for reordering) - func updateSortOrders(_ items: [CustomItineraryItem]) async throws { - guard !items.isEmpty else { return } - print("☁️ [CloudKit] Batch updating day+sortOrder for \(items.count) items") - - // Fetch all records - let recordIDs = items.map { CKRecord.ID(recordName: $0.id.uuidString) } - let fetchResults = try await publicDatabase.records(for: recordIDs) - - // Update each record's day and sortOrder - var recordsToSave: [CKRecord] = [] - let now = Date() - - for item in items { - let recordID = CKRecord.ID(recordName: item.id.uuidString) - guard case .success(let record) = fetchResults[recordID] else { continue } - - record[CKCustomItineraryItem.dayKey] = item.day - record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder - record[CKCustomItineraryItem.modifiedAtKey] = now - recordsToSave.append(record) - } - - // Save all in one batch operation - let modifyOp = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil) - modifyOp.savePolicy = .changedKeys - - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - modifyOp.modifyRecordsResultBlock = { result in - switch result { - case .success: - print("☁️ [CloudKit] Batch day+sortOrder update complete") - continuation.resume() - case .failure(let error): - print("☁️ [CloudKit] Batch update failed: \(error)") - continuation.resume(throwing: error) - } - } - publicDatabase.add(modifyOp) - } - } - - func deleteItem(_ itemId: UUID) async throws { - let recordID = CKRecord.ID(recordName: itemId.uuidString) - - do { - try await publicDatabase.deleteRecord(withID: recordID) - } catch let error as CKError { - if error.code != .unknownItem { - throw mapCloudKitError(error) - } - // Item already deleted - ignore - } catch { - throw CustomItemError.unknown(error) - } - } - - func fetchItems(forTripId tripId: UUID) async throws -> [CustomItineraryItem] { - print("☁️ [CloudKit] Fetching items for tripId: \(tripId.uuidString)") - let predicate = NSPredicate( - format: "%K == %@", - CKCustomItineraryItem.tripIdKey, - tripId.uuidString - ) - let query = CKQuery(recordType: CKRecordType.customItineraryItem, predicate: predicate) - - do { - let (results, _) = try await publicDatabase.records(matching: query) - print("☁️ [CloudKit] Query returned \(results.count) records") - - let items = results.compactMap { result -> CustomItineraryItem? in - guard case .success(let record) = result.1 else { - print("☁️ [CloudKit] Record fetch failed for: \(result.0.recordName)") - return nil - } - let item = CKCustomItineraryItem(record: record).toItem() - if item == nil { - print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)") - } - return item - }.sorted { ($0.day, $0.sortOrder) < ($1.day, $1.sortOrder) } - - print("☁️ [CloudKit] Parsed \(items.count) valid items") - return items - } catch let error as CKError { - print("☁️ [CloudKit] CKError on fetch: \(error.code.rawValue) - \(error.localizedDescription)") - throw mapCloudKitError(error) - } catch { - print("☁️ [CloudKit] Unknown error on fetch: \(error)") - throw CustomItemError.unknown(error) - } - } - - // MARK: - Error Mapping - - private func mapCloudKitError(_ error: CKError) -> CustomItemError { - switch error.code { - case .notAuthenticated: - return .notSignedIn - case .networkUnavailable, .networkFailure: - return .networkUnavailable - case .unknownItem: - return .itemNotFound - default: - return .unknown(error) - } - } -} - -// MARK: - Errors - -enum CustomItemError: Error, LocalizedError { - case notSignedIn - case itemNotFound - case networkUnavailable - case unknown(Error) - - var errorDescription: String? { - switch self { - case .notSignedIn: - return "Please sign in to iCloud to use custom items." - case .itemNotFound: - return "Item not found. It may have been deleted." - case .networkUnavailable: - return "Unable to connect. Please check your internet connection." - case .unknown(let error): - return "An error occurred: \(error.localizedDescription)" - } - } -} diff --git a/SportsTime/Core/Services/CustomItemSubscriptionService.swift b/SportsTime/Core/Services/CustomItemSubscriptionService.swift deleted file mode 100644 index 264706f..0000000 --- a/SportsTime/Core/Services/CustomItemSubscriptionService.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// CustomItemSubscriptionService.swift -// SportsTime -// -// CloudKit subscription service for real-time custom item updates -// - -import Foundation -import CloudKit -import Combine - -/// Manages CloudKit subscriptions for custom itinerary items on shared trips -actor CustomItemSubscriptionService { - static let shared = CustomItemSubscriptionService() - - private let container: CKContainer - private let publicDatabase: CKDatabase - - /// Publisher that emits trip IDs when their custom items change - private let changeSubject = PassthroughSubject() - var changePublisher: AnyPublisher { - changeSubject.eraseToAnyPublisher() - } - - /// Track which trip IDs have active subscriptions - private var subscribedTripIds: Set = [] - - private init() { - self.container = CKContainer(identifier: "iCloud.com.sportstime.app") - self.publicDatabase = container.publicCloudDatabase - } - - // MARK: - Subscription Management - - /// Subscribe to changes for a specific trip's custom items - func subscribeToTrip(_ tripId: UUID) async throws { - guard !subscribedTripIds.contains(tripId) else { - print("📡 [Subscription] Already subscribed to trip: \(tripId)") - return - } - - let subscriptionID = "custom-items-\(tripId.uuidString)" - - // Check if subscription already exists - do { - _ = try await publicDatabase.subscription(for: subscriptionID) - subscribedTripIds.insert(tripId) - print("📡 [Subscription] Found existing subscription for trip: \(tripId)") - return - } catch let error as CKError where error.code == .unknownItem { - // Subscription doesn't exist, create it - } catch { - print("📡 [Subscription] Error checking subscription: \(error)") - } - - // Create subscription for this trip's custom items - let predicate = NSPredicate( - format: "%K == %@", - CKCustomItineraryItem.tripIdKey, - tripId.uuidString - ) - - let subscription = CKQuerySubscription( - recordType: CKRecordType.customItineraryItem, - predicate: predicate, - subscriptionID: subscriptionID, - options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] - ) - - // Configure notification - let notificationInfo = CKSubscription.NotificationInfo() - notificationInfo.shouldSendContentAvailable = true // Silent notification - subscription.notificationInfo = notificationInfo - - do { - try await publicDatabase.save(subscription) - subscribedTripIds.insert(tripId) - print("📡 [Subscription] Created subscription for trip: \(tripId)") - } catch let error as CKError { - print("📡 [Subscription] Failed to create subscription: \(error.code.rawValue) - \(error.localizedDescription)") - throw error - } - } - - /// Unsubscribe from a specific trip's custom items - func unsubscribeFromTrip(_ tripId: UUID) async throws { - guard subscribedTripIds.contains(tripId) else { return } - - let subscriptionID = "custom-items-\(tripId.uuidString)" - - do { - try await publicDatabase.deleteSubscription(withID: subscriptionID) - subscribedTripIds.remove(tripId) - print("📡 [Subscription] Removed subscription for trip: \(tripId)") - } catch let error as CKError where error.code == .unknownItem { - // Already deleted - subscribedTripIds.remove(tripId) - } - } - - /// Subscribe to all trips that the user has access to (via polls) - func subscribeToSharedTrips(tripIds: [UUID]) async { - for tripId in tripIds { - do { - try await subscribeToTrip(tripId) - } catch { - print("📡 [Subscription] Failed to subscribe to trip \(tripId): \(error)") - } - } - } - - // MARK: - Notification Handling - - /// Handle CloudKit notification - call this from AppDelegate/SceneDelegate - func handleNotification(_ notification: CKNotification) { - guard let queryNotification = notification as? CKQueryNotification, - let subscriptionID = queryNotification.subscriptionID, - subscriptionID.hasPrefix("custom-items-") else { - return - } - - // Extract trip ID from subscription ID - let tripIdString = String(subscriptionID.dropFirst("custom-items-".count)) - guard let tripId = UUID(uuidString: tripIdString) else { return } - - print("📡 [Subscription] Received change notification for trip: \(tripId)") - changeSubject.send(tripId) - } - - /// Handle notification from userInfo dictionary (from push notification) - nonisolated func handleRemoteNotification(userInfo: [AnyHashable: Any]) async { - guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else { - return - } - await handleNotification(notification) - } - - // MARK: - Cleanup - - /// Remove all subscriptions (call on sign out or cleanup) - func removeAllSubscriptions() async { - for tripId in subscribedTripIds { - try? await unsubscribeFromTrip(tripId) - } - subscribedTripIds.removeAll() - } -} diff --git a/SportsTime/Core/Services/TravelOverrideService.swift b/SportsTime/Core/Services/TravelOverrideService.swift deleted file mode 100644 index c6d003f..0000000 --- a/SportsTime/Core/Services/TravelOverrideService.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// TravelOverrideService.swift -// SportsTime -// -// CloudKit service for travel day position overrides -// - -import Foundation -import CloudKit - -actor TravelOverrideService { - static let shared = TravelOverrideService() - - private let container: CKContainer - private let publicDatabase: CKDatabase - - private init() { - self.container = CKContainer(identifier: "iCloud.com.sportstime.app") - self.publicDatabase = container.publicCloudDatabase - } - - // MARK: - CRUD Operations - - /// Create or update a travel day override (upserts by tripId + travelAnchorId) - func saveOverride(_ override: TravelDayOverride) async throws -> TravelDayOverride { - print("☁️ [CloudKit] Saving travel override for: \(override.travelAnchorId)") - - // Look up existing override by travelAnchorId (not by UUID) - let existingOverrides = try await fetchOverrides(forTripId: override.tripId) - let existingOverride = existingOverrides.first { $0.travelAnchorId == override.travelAnchorId } - - let record: CKRecord - let now = Date() - - if let existing = existingOverride { - // Fetch the actual CKRecord to update it - let recordID = CKRecord.ID(recordName: existing.id.uuidString) - do { - let existingRecord = try await publicDatabase.record(for: recordID) - existingRecord[CKTravelDayOverride.displayDayKey] = override.displayDay - existingRecord[CKTravelDayOverride.modifiedAtKey] = now - record = existingRecord - print("☁️ [CloudKit] Updating existing override record: \(existing.id)") - } catch { - // Record doesn't exist anymore, create new - let ckOverride = CKTravelDayOverride(override: override) - record = ckOverride.record - } - } else { - // Create new record - let ckOverride = CKTravelDayOverride(override: override) - record = ckOverride.record - print("☁️ [CloudKit] Creating new override record") - } - - do { - try await publicDatabase.save(record) - print("☁️ [CloudKit] Travel override saved: \(record.recordID.recordName)") - var savedOverride = override - savedOverride.modifiedAt = now - return savedOverride - } catch let error as CKError { - print("☁️ [CloudKit] CKError on save: \(error.code.rawValue) - \(error.localizedDescription)") - throw mapCloudKitError(error) - } catch { - print("☁️ [CloudKit] Unknown error on save: \(error)") - throw TravelOverrideError.unknown(error) - } - } - - /// Delete a travel day override - func deleteOverride(_ overrideId: UUID) async throws { - let recordID = CKRecord.ID(recordName: overrideId.uuidString) - - do { - try await publicDatabase.deleteRecord(withID: recordID) - print("☁️ [CloudKit] Travel override deleted: \(overrideId)") - } catch let error as CKError { - if error.code != .unknownItem { - throw mapCloudKitError(error) - } - // Already deleted - ignore - } catch { - throw TravelOverrideError.unknown(error) - } - } - - /// Delete override by travel anchor ID (useful when removing by travel segment) - func deleteOverride(forTripId tripId: UUID, travelAnchorId: String) async throws { - // First fetch to find the record - let overrides = try await fetchOverrides(forTripId: tripId) - if let existing = overrides.first(where: { $0.travelAnchorId == travelAnchorId }) { - try await deleteOverride(existing.id) - } - } - - /// Fetch all travel day overrides for a trip - func fetchOverrides(forTripId tripId: UUID) async throws -> [TravelDayOverride] { - print("☁️ [CloudKit] Fetching travel overrides for tripId: \(tripId.uuidString)") - let predicate = NSPredicate( - format: "%K == %@", - CKTravelDayOverride.tripIdKey, - tripId.uuidString - ) - let query = CKQuery(recordType: CKRecordType.travelDayOverride, predicate: predicate) - - do { - let (results, _) = try await publicDatabase.records(matching: query) - print("☁️ [CloudKit] Query returned \(results.count) travel override records") - - let overrides = results.compactMap { result -> TravelDayOverride? in - guard case .success(let record) = result.1 else { - print("☁️ [CloudKit] Record fetch failed for: \(result.0.recordName)") - return nil - } - let override = CKTravelDayOverride(record: record).toOverride() - if override == nil { - print("☁️ [CloudKit] Failed to parse travel override record: \(record.recordID.recordName)") - } - return override - } - - print("☁️ [CloudKit] Parsed \(overrides.count) valid travel overrides") - return overrides - } catch let error as CKError { - print("☁️ [CloudKit] CKError on fetch: \(error.code.rawValue) - \(error.localizedDescription)") - throw mapCloudKitError(error) - } catch { - print("☁️ [CloudKit] Unknown error on fetch: \(error)") - throw TravelOverrideError.unknown(error) - } - } - - /// Convert array of overrides to dictionary keyed by travelAnchorId - /// If duplicate travelAnchorIds exist (legacy data), takes the most recently modified one - func fetchOverridesAsDictionary(forTripId tripId: UUID) async throws -> [String: Int] { - let overrides = try await fetchOverrides(forTripId: tripId) - // Sort by modifiedAt descending so most recent comes first - let sorted = overrides.sorted { $0.modifiedAt > $1.modifiedAt } - // Use uniquingKeysWith to keep the first (most recent) value for duplicates - return Dictionary(sorted.map { ($0.travelAnchorId, $0.displayDay) }) { first, _ in first } - } - - // MARK: - Error Mapping - - private func mapCloudKitError(_ error: CKError) -> TravelOverrideError { - switch error.code { - case .notAuthenticated: - return .notSignedIn - case .networkUnavailable, .networkFailure: - return .networkUnavailable - case .unknownItem: - return .overrideNotFound - default: - return .unknown(error) - } - } -} - -// MARK: - Errors - -enum TravelOverrideError: Error, LocalizedError { - case notSignedIn - case overrideNotFound - case networkUnavailable - case unknown(Error) - - var errorDescription: String? { - switch self { - case .notSignedIn: - return "Please sign in to iCloud to customize travel days." - case .overrideNotFound: - return "Travel day setting not found." - case .networkUnavailable: - return "Unable to connect. Please check your internet connection." - case .unknown(let error): - return "An error occurred: \(error.localizedDescription)" - } - } -} diff --git a/SportsTime/Features/Trip/Views/CustomItemRow.swift b/SportsTime/Features/Trip/Views/CustomItemRow.swift deleted file mode 100644 index 4ca2af9..0000000 --- a/SportsTime/Features/Trip/Views/CustomItemRow.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// CustomItemRow.swift -// SportsTime -// -// Row component for custom itinerary items with drag handle -// - -import SwiftUI - -struct CustomItemRow: View { - @Environment(\.colorScheme) private var colorScheme - - let item: CustomItineraryItem - var onTap: () -> Void - var onDelete: () -> Void - - // Drag handle visible - users can drag to reorder using .draggable/.dropDestination - - var body: some View { - Button(action: onTap) { - HStack(spacing: 12) { - // Drag handle - Image(systemName: "line.3.horizontal") - .foregroundStyle(.tertiary) - .font(.caption) - - // Category icon - Text(item.category.icon) - .font(.title3) - - // Title - Text(item.title) - .font(.subheadline) - .foregroundStyle(Theme.textPrimary(colorScheme)) - .lineLimit(2) - - Spacer() - - // Chevron to indicate tappable (tap to edit) - Image(systemName: "chevron.right") - .foregroundStyle(.tertiary) - .font(.caption) - } - .padding(.horizontal, Theme.Spacing.md) - .padding(.vertical, Theme.Spacing.sm) - .background { - // Opaque base + semi-transparent accent - RoundedRectangle(cornerRadius: 8) - .fill(Theme.cardBackground(colorScheme)) - RoundedRectangle(cornerRadius: 8) - .fill(Theme.warmOrange.opacity(0.1)) - } - .overlay { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(Theme.warmOrange.opacity(0.3), lineWidth: 1) - } - } - .buttonStyle(.plain) - .contextMenu { - Button { - onTap() - } label: { - Label("Edit", systemImage: "pencil") - } - Button(role: .destructive) { - onDelete() - } label: { - Label("Delete", systemImage: "trash") - } - } - } -} - -#Preview { - VStack { - CustomItemRow( - item: CustomItineraryItem( - tripId: UUID(), - category: .restaurant, - title: "Joe's BBQ - Best brisket in Texas!", - day: 1 - ), - onTap: {}, - onDelete: {} - ) - CustomItemRow( - item: CustomItineraryItem( - tripId: UUID(), - category: .hotel, - title: "Hilton Downtown", - day: 1 - ), - onTap: {}, - onDelete: {} - ) - } - .padding() -}